一、前言
1、上篇.NET Core ResponseCache【快取篇(一)】中我們提到了使用客戶端快取、和服務端快取。本文我們介紹MemoryCache快取元件,說到服務端快取我們一般都會想到MemoryCache、Redis等等優秀的快取元件,各自有各自使用的場景。MemoryCache的型別比較單一是Object物件儲存、Redis的資料型別就相對比較多 String(字串),List(列表),set(去重集合),zset(去重排序集合),hash(雜湊)。還有HyperLogLog,bitMap,GeoHash,BloomFilter這四種還沒有詳細瞭解,等下篇講解Redis的時候詳細給各位姥爺供上。
二、MemoryCache快取元件使用
1、首先我們需要將MemoryCache元件注入到程式中。
public void ConfigureServices(IServiceCollection services) { //新增記憶體快取 services.AddMemoryCache(); }
2、使用的方法也比較簡單和我們平常寫程式碼差不多。在控制其中注入基類IMemoryCache,看到下面這個基類,我還以為會只有兩個方法。
#region 注入快取 private readonly IMemoryCache Cache; public MemoryCacheController(IMemoryCache cache) { Cache = cache; } #endregion
3、當我正常使用的時候發現方法原來使用推展方法寫在這裡。使用起來很簡單我就直接程式碼了。
4、設定快取主要是四種過期時間。
1:永久不過期 就是老子就是不過期氣死你。
2:絕對過期時間 這個就好比我們買的安眠藥,2020年07月22日過期。那你今天就要好好利用這個時間了。
3:相對過期時間 這個好比你吃飯三個小時,肚子就呱呱叫了,相對於當前時間延長。
4:滑動過期時間 這個就是相對過期時間的升級版,我們每隔三個小時就會肚子餓,那麼我們是不是可以等到兩個半小時就是吃飯,肚子就不會叫了。這樣在一定時間內使用這個快取就會在你最後使用快取的時間上延長。
//設定快取 當我們沒有指定過期時間,理論是永久的(後面會說到不理論的) Cache.Set("key", "value"); Console.WriteLine(Cache.Get("key")); //設定快取絕對過期時間 Cache.Set("key1", "value", new DateTimeOffset(new DateTime(2020, 7, 22))); Console.WriteLine(Cache.Get("key1")); //設定相對過期時間 Cache.Set("key2", "value", new TimeSpan(0, 0, 10)); Console.WriteLine(Cache.Get("key2")); //設定滑動過期時間 Cache.Set("key3", "value", new MemoryCacheEntryOptions() { //設定滑動過期 SlidingExpiration = new TimeSpan(0, 0, 5), //設定快取的優先順序,當我們快取空間不足的時候會移除等級低的快取,以此類推(清除的時候不會管是否過期) //Low, 低的意思 //Normal, 正常模式 預設模式 //High, 高 //NeverRemove 絕不回收,這個就算記憶體滿了也不會清除。 Priority = CacheItemPriority.NeverRemove }); Console.WriteLine(Cache.Get("key3"));
5、這裡我們還要講一下快取失效機制,這裡它不是主動失效的,只有當我們再次對快取MemoryCache元件進行增刪查改的時候才會掃描裡面的記憶體是否存在過期的,進行垃圾回收。這裡我們使用GetOrCreate方法建立我們的快取證實我們的說法。這個可以設定過期回撥函式。圖下我們可以看到當我們的快取過期之後,就沒有對快取進行操作了就不會有回撥函式觸發。
Cache.GetOrCreate("key4", cacheEntry => { //設定滑動過期 cacheEntry.SlidingExpiration= new TimeSpan(0, 0, 5); //設定刪除回撥函式 cacheEntry.RegisterPostEvictionCallback(CallbackFunction); //設定記憶體 return cacheEntry.Value = "滑動過期時間帶刪除回撥"; }); Thread.Sleep(2000); Console.WriteLine(Cache.Get("key4")); Thread.Sleep(4000); Console.WriteLine(Cache.Get("key4")); Thread.Sleep(5000);
6、但是我們將最後一句程式碼解開封印。這裡有一個問題,當我們執行等待了5秒鐘按道理過期了,但是沒有觸發刪除回撥,我截圖上的是我將程式碼塊拉到了之前執行的最後一行程式碼才有,這個我也蒙了。但是當我第二次進來訪問就有回撥函式了。
/// <summary> /// 刪除回撥函式 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="reason"></param> /// <param name="state"></param> public void CallbackFunction(object key, object value, EvictionReason reason, object state) { Console.WriteLine($"你被刪除了key:{key},value:{value}回撥函式"); }
7、CreateEntry設定快取,有一點特殊正常設定快取的時候,獲取的時候是null,只有當我們執行Dispose或者using的時候才成功。帶著這個疑問我們決定看看原始碼。
////CreateEntry設定快取 var entity = Cache.CreateEntry("key5"); entity.Value = "5555"; Console.WriteLine("CreateEntry 獲取資訊:" + Cache.Get("key5")); //結果null
//方法1 var entity = Cache.CreateEntry("key5"); entity.Value = "5555"; entity.Dispose(); Console.WriteLine("CreateEntry 獲取資訊:" + Cache.Get("key5")); //結果5555 //方法2 using (var entity = Cache.CreateEntry("key5")) { entity.Value = "5555"; Console.WriteLine("CreateEntry 獲取資訊:" + Cache.Get("key5")); //結果5555 }
8、CacheEntry有一個重要的方法Dispose(),因為它繼承IDisposable,在Dispost方法中呼叫了_notifyCacheEntryDisposed委託。
public void Dispose() { if (!_added) { _added = true; _scope.Dispose(); _notifyCacheEntryDisposed(this);//在此呼叫委託,而此委託是被MemoryCache類中的SetEntry賦值。目的是將CacheEntry實體放入MemoryCache類的字典中,也就是放入快取中。 PropagateOptions(CacheEntryHelper.Current);
三、MemoryCache元件原始碼
1、首先看我們在Startup中宣告新增的記憶體快取的對應的原始碼。 services.AddMemoryCache();可以看到這裡是將快取元件使用單例模式注入我們程式中。
/// <summary> /// Adds a non distributed in memory implementation of <see cref="IMemoryCache"/> to the /// <see cref="IServiceCollection" />. /// </summary> /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> public static IServiceCollection AddMemoryCache(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddOptions(); services.TryAdd(ServiceDescriptor.Singleton<IMemoryCache, MemoryCache>()); return services; }
2、MemoryCacheOptions類:主要是配置一些引數
ExpirationScanFrequency:此欄位表明隔一段時間掃描快取,移除過期快取,預設頻率為一分鐘。
SizeLimit:設定快取的大小。
CompactionPercentage:壓縮比例 預設為百分之五。
//新增記憶體快取 services.AddMemoryCache(x=> { x.CompactionPercentage = 0.05;//快取壓縮大小 x.ExpirationScanFrequency= new TimeSpan(0, 0, 1); //預設自動掃描時間為三分鐘 x.SizeLimit = 500 * 1024 * 1024; //大小為500M });
3、MemoryCache類
1:ConcurrentDictionary<object, CacheEntry> _entries:一個多執行緒安全的字典型別,其實快取的本質就是這個字典,將所有快取都放入這個字典中,然後通過字典的key(字典的key其實和快取實體CacheEntry的key值一樣)獲取CacheEntry實體(CacheEntry實體包含key和value,也就是我們程式碼中設定的key和value)。
2:_expirationScanFrequency:Span型別,表示掃描過期快取的頻率時間,此欄位的值來自MemoryCacheOptions類的ExpirationScanFrequency。需要注意的是:假如設定為一分鐘掃描一次快取集合,這個掃描不是主動掃描的,只有當對快取集合操作時才會掃描。比如增刪改查快取集合的時候,會判斷上一次掃描的時間離現在過去多久了,如果超過掃描設定的時間,才會掃描。
private void StartScanForExpiredItems()//掃描函式。在新增、查詢、刪除、修改快取的時候,會呼叫此方法掃描過濾過期快取,並不是主動隔一段時間執行 { var now = _clock.UtcNow; if (_expirationScanFrequency < now - _lastExpirationScan)//判斷現在的時間和最後一次掃描時間,是不是大於設定的時間段 { _lastExpirationScan = now; Task.Factory.StartNew(state => ScanForExpiredItems((MemoryCache)state), this, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } } private static void ScanForExpiredItems(MemoryCache cache) { var now = cache._clock.UtcNow; foreach (var entry in cache._entries.Values)//然後遍歷字典集合,移除過期快取 { if (entry.CheckExpired(now)) { cache.RemoveEntry(entry); } } }