目錄
- 緩衝的基本概念
- 緩衝原理
- 緩衝設計
- 分布式緩衝 Memcache 與 Redis 的比較
- 緩衝穿透,緩衝擊穿,緩衝雪崩解決方案
- 資料一致性
- 使用內建 MemoryCache
- 使用分布式緩衝 Redis
- 使用 Stackexchange.Redis 自己封裝一個 RedisHelper 類
- 參考
緩衝的基本概念
緩衝是分布式系統中的重要組件,主要解決高並發,巨量資料情境下,熱點資料訪問的效能問題。提供高效能的資料快速存取。
緩衝原理
- 將資料寫入到讀取速度更快的存放裝置;
- 將資料緩衝到離應用最近的位置;
- 將資料緩衝到離使用者最近的位置。
緩衝設計
- 緩衝內容 熱點資料,靜態資源
- 緩衝位置 CDN,反向 Proxy,分布式快取服務器,本機(記憶體,硬碟)
CDN:存放HTML、CSS、JS等靜態資源;
反向 Proxy:動靜分離,只緩衝使用者請求的靜態資源;
分布式緩衝:快取資料庫中的熱點資料;
本機快取:緩衝應用字典等常用資料。
- 到期策略 固定時間,相對時間
- 同步機制 即時寫入,非同步重新整理
分布式緩衝 Memcache 與 Redis 的比較
- 資料結構:Memcache只支援key value儲存方式,Redis支援更多的資料類型,比如Key value、hash、list、set、zset;
- 多線程:Memcache支援多線程,Redis支援單線程;CPU利用方面Memcache優於Redis;
- 持久化:Memcache不支援持久化,Redis支援持久化(快照和AOF日誌兩種持久化方式);
- 記憶體利用率:Memcache高,Redis低(採用壓縮的情況下比Memcache高)。使用簡單的key-value儲存的話,Memcached的記憶體利用率更高,而如果Redis採用hash結構來做key-value儲存,由於其組合式的壓縮,其記憶體利用率會高於Memcache。
- 到期策略:Memcache到期後,不刪除緩衝,會導致下次取資料資料的問題,Redis有專門線程,清除快取資料;
緩衝穿透,緩衝擊穿,緩衝雪崩解決方案
- 緩衝穿透
緩衝穿透是指查詢一個一定不存在的資料。由於會頻繁的請求資料庫,對資料庫造成訪問壓力。
解決方案:
- 緩衝雪崩
緩衝雪崩是指在我們設定緩衝時採用了相同的到期時間,導致緩衝在某一時刻同時失效,請求全部轉寄到DB,DB瞬時壓力過重雪崩。
解決方案:
- 通過加鎖或者隊列來控制讀資料庫寫緩衝的線程數量。比如對某個key只允許一個線程查詢資料和寫緩衝,其他線程等待。
- 分散緩衝失效時間,比如在設定到期時間的時候增加一個隨機數儘可能的保證緩衝不會大面積的同時失效。
- 緩衝擊穿
緩衝擊穿是指對於一些設定了到期時間的key,如果這些key可能會在到期後的某個時間點被超高並發地訪問。這個和緩衝雪崩的區別在於這裡針對某一key緩衝,前者則是很多key。
解決方案:
- 使用互斥鎖來解決問題,通俗的描述就是,一萬個使用者訪問了,但是只有一個使用者可以拿到訪問資料庫的許可權,當這個使用者拿到這個許可權之後重新建立緩衝,這個時候剩下的訪問者因為沒有拿到許可權,就原地等待著去訪問緩衝。
資料一致性
資料不一致的幾種情況:
- 資料庫有資料,緩衝沒有資料;
- 資料庫有資料,緩衝也有資料,資料不相等;
- 資料庫沒有資料,緩衝有資料。
目前比較常用的資料緩衝策略的是Cache Aside Pattern,更新緩衝是先把資料存到資料庫中,成功後,再讓緩衝失效。
這種策略下不一致產生的原因只有更新資料庫成功,但是刪除緩衝失敗。
解決方案:
- 對刪除緩衝進行重試.
- 定期全量更新緩衝。
- 合理設定緩衝到期時間。
使用內建 MemoryCache
ASP.NET Core 支援多種不同的緩衝。包括記憶體緩衝,分布式緩衝(Redis 和 SQL Server)。Github 開源地址 Libraries for in-memory caching and distributed caching.
IMemoryCache是把資料存放區在Web伺服器的記憶體中。
在 ConfigureServices 中調用 AddMemoryCache 通過依賴關係注入引用服務。
public void ConfigureServices(IServiceCollection services){ services.AddMemoryCache(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);}
在控制器類中用構造器注入的方式建立 IMemoryCache 的對象。
using Microsoft.Extensions.Caching.Memory;public class ValuesController : ControllerBase{ private IMemoryCache _cache; public ValuesController(IMemoryCache cache) { _cache = cache; }}
- 一些常用操作
建立緩衝
Set()
DateTime cacheEntry1 = DateTime.Now;var cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(3));_cache.Set("cache1", cacheEntry1, cacheEntryOptions);
GetOrCreate() GetOrCreateAsync
var cacheEntry = _cache.GetOrCreate("cache1", entry => { entry.SetAbsoluteExpiration(TimeSpan.FromSeconds(3)); return DateTime.Now;});
擷取緩衝
Get()
var cacheEntry = this._cache.Get<DateTime?>("cache1");
TryGetValue()
DateTime cacheEntry;if (!_cache.TryGetValue("cache1", out cacheEntry)){ // Key not in cache, so get data. cacheEntry = DateTime.Now; var cacheEntryOptions = new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromSeconds(3)); _cache.Set("cache1", cacheEntry, cacheEntryOptions);}
刪除緩衝
Remove()
_cache.Remove("cache1");
其他知識點
ICacheEntry成員:
Key 緩衝key
Value 緩衝值
AbsoluteExpiration 絕對到期時間,為null則條件無效
AbsoluteExpirationRelativeToNow 相對目前時間的絕對到期時間(使用TimeSpan),為null條件無效
SlidingExpiration 滑動到期時間
ExpirationTokens 提供用來自訂緩衝到期
PostEvictionCallbacks 緩衝失效回調
Priority 快取項目優先順序(在緩衝滿載的時候絕對清除的順序)
Size 代表快取資料的大小,在記憶體緩衝中一般為null
緩衝到期的方式
- 絕對到期(指定在一個固定的時間點到期)
- 滑動到期(在一個時間長度內沒有被命中則到期,如果命中則順延)
- 到期Token(自訂到期)
使用分布式緩衝 Redis
- Nuget 安裝 Microsoft.Extensions.Caching.Redis
ConfigureServices 方法裡面添加服務 AddDistributedRedisCache
public void ConfigureServices(IServiceCollection services){ services.AddDistributedRedisCache(options => { options.Configuration = "localhost"; options.InstanceName = "Instance1"; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);}
- 常用操作
RedisCache 實現了 IDistributedCache 介面,提供了常用的添加、檢索和刪除操作。
Get,GetAsync 採用字串鍵並以byte[]形式檢索快取項目(如果在緩衝中找到)
Set,SetAsync 使用字串鍵向緩衝添加項byte[]形式
Refresh,RefreshAsync 根據鍵重新整理緩衝中的項,並重設其可調到期逾時值(如果有)
Remove,RemoveAsync 根據鍵刪除快取項目
var now = DateTime.Now;var cacheValue = System.Text.Encoding.UTF8.GetBytes(now.ToString());var options = new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(3));_cache.Set("cache1", cacheValue, options);_cache.Refresh("cache1");var value = _cache.Get("cache1");var nowString = System.Text.Encoding.UTF8.GetString(value);_cache.Remove("cache1");
由於內建的 RedisCache 繼承 IDistributedCache 介面並沒有提供 Redis的一些進階特性比如Hash, List, Set等。
使用 Stackexchange.Redis 自己封裝一個 RedisHelper 類
基於Stackexchange.Redis封裝一個簡單RedisHelper類:
RedisHelper 類
public class RedisHelper{ private readonly RedisOptions _options; private readonly Lazy<ConnectionMultiplexer> _connectionMultiplexer; public RedisHelper(IOptions<RedisOptions> optionsAccessor) { if (optionsAccessor == null) { throw new ArgumentNullException(nameof(optionsAccessor)); } _options = optionsAccessor.Value; _connectionMultiplexer = new Lazy<ConnectionMultiplexer>(CreateConnectionMultiplexer); } public IDatabase GetDatabase() { return _connectionMultiplexer.Value.GetDatabase(); } private ConnectionMultiplexer CreateConnectionMultiplexer() { if (_options.ConfigurationOptions != null) { return ConnectionMultiplexer.Connect(_options.ConfigurationOptions); } else { return ConnectionMultiplexer.Connect(_options.Configuration); } }}
RedisOptions 配置類
public class RedisOptions : IOptions<RedisOptions>{ /// <summary> /// The configuration used to connect to Redis. /// </summary> public string Configuration { get; set; } /// <summary> /// The configuration used to connect to Redis. /// This is preferred over Configuration. /// </summary> public ConfigurationOptions ConfigurationOptions { get; set; } /// <summary> /// The Redis instance name. /// </summary> public string InstanceName { get; set; } RedisOptions IOptions<RedisOptions>.Value { get { return this; } }}
RedisHelperServiceCollectionExtensions 擴充類
public static class RedisHelperServiceCollectionExtensions{ public static IServiceCollection AddRedisHelper(this IServiceCollection services, Action<RedisOptions> setupAction) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (setupAction == null) { throw new ArgumentNullException(nameof(setupAction)); } services.AddOptions(); services.Configure(setupAction); services.AddSingleton<RedisHelper>(); return services; }}
在 ConfigureServices 裡面加入服務參考
public void ConfigureServices(IServiceCollection services){ var redisOptions = Configuration.GetSection("RedisOptions").Get<RedisOptions>(); services.AddRedisHelper(options => { options.Configuration = redisOptions.Configuration; options.InstanceName = redisOptions.InstanceName; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);}
在 Controller 裡面使用
public class ValuesController : ControllerBase{ private readonly RedisHelper _redisHelper; public ValuesController(RedisHelper redisHelper) { _redisHelper = redisHelper; } // GET api/values [HttpGet] public ActionResult<IEnumerable<string>> Get() { _redisHelper.GetDatabase().StringSet("test_key_2", "test_value_2", TimeSpan.FromSeconds(60)); return new string[] { "value1", "value2" }; }}
網上一些開源的Redis擴充:
- StackExchange.Redis.Extensions
- EasyCaching
參考
- 緩衝技術的詳解
- 緩衝更新的套路
- 緩衝擊穿/穿透/雪崩
- Cache in ASP.NET Core
- 擁抱.NET Core系列:MemoryCache 緩衝到期
- 使用Redis實現分布式緩衝