關於 ASP.NET Core IMemoryCache RegisterPostEvictionCallback 的觸發時機

同事提到想用 ASP.NET Core 的 IMemoryCache 來處理 application 本身的 cache,無奈小弟學藝不精沒有太多想法可以參與討論,所以趕緊惡補,藉這個機會學習也順便做些 POC,大致上與過去使用 System.Runtime.CachingMemoryCache 用法概念接近,但實際用法則不全然相同

基本比較

- MemoryCache(ASP.NET Core) MemoryCache(.NET Framework)
Namespace Microsoft.Extensions.Caching.Memory System.Runtime.Caching
Assembly Microsoft.Extensions.Caching.Memory.dll System.Runtime.Caching.dll
繼承 Object –> MemoryCache Object –> ObjectCache –> MemoryCache
實作 IMemoryCache,IDisposable IEnumerable,IDisposable
建構子 MemoryCache(IOptions<MemoryCacheOptions>) MemoryCache(String, NameValueCollection)
MemoryCache(String, NameValueCollection, Boolean)
屬性 Count CacheMemoryLimit
Default
DefaultCacheCapabilities
Item[String]
Name
PhysicalMemoryLimit
PollingInterval
方法 Compact(Double)
CreateEntry(Object)
Dispose()
Dispose(Boolean)
Finalize()
Remove(Object)
TryGetValue(Object, Object)
Add(CacheItem, CacheItemPolicy)
AddOrGetExisting(CacheItem, CacheItemPolicy)
AddOrGetExisting(String, Object, CacheItemPolicy, String)
AddOrGetExisting(String, Object, DateTimeOffset, String)
Contains(String, String)
CreateCacheEntryChangeMonitor(IEnumerable<String>, String)
Dispose()
Get(String, String)
GetCacheItem(String, String)
GetCount(String)
GetEnumerator()
GetLastSize(String)
GetValues(IEnumerable, String)
Remove(String, CacheEntryRemovedReason, String)
Remove(String, String)
Set(CacheItem, CacheItemPolicy)
Set(String, Object, CacheItemPolicy, String)
Set(String, Object, DateTimeOffset, String)
Trim(Int32)
明確介面實作 - IEnumerable.GetEnumerator()
擴充方法 Get(IMemoryCache, Object)
Get<TItem>(IMemoryCache, Object)
GetOrCreate<TItem>(IMemoryCache, Object, Func)
GetOrCreateAsync<TItem>(IMemoryCache, Object, Func<ICacheEntry,Task<TItem>>)
Set<TItem>(IMemoryCache, Object, TItem)
Set<TItem>(IMemoryCache, Object, TItem, MemoryCacheEntryOptions)
Set<TItem>(IMemoryCache, Object, TItem, IChangeToken)
Set<TItem>(IMemoryCache, Object, TItem, DateTimeOffset)
Set<TItem>(IMemoryCache, Object, TItem, TimeSpan)
TryGetValue<TItem>(IMemoryCache, Object, TItem)
CopyToDataTable<T>(IEnumerable<T>)
CopyToDataTable<T>(IEnumerable<T>, DataTable, LoadOption)
CopyToDataTable<T>(IEnumerable<T>, DataTable, LoadOption, FillErrorEventHandler)
Cast<TResult>(IEnumerable)
OfType<TResult>(IEnumerable)
AsParallel(IEnumerable)
AsQueryable(IEnumerable)
Ancestors<T>(IEnumerable<T>)
Ancestors<T>(IEnumerable<T>, XName)
DescendantNodes<T>(IEnumerable<T>)
Descendants<T>(IEnumerable<T>)
Descendants<T>(IEnumerable<T>, XName)
Elements<T>(IEnumerable<T>)
Elements<T>(IEnumerable<T>, XName)
InDocumentOrder<T>(IEnumerable<T>)
Nodes<T>(IEnumerable<T>)
Remove<T>(IEnumerable<T>)

基本環境說明

  1. macOS Mojave 10.14.4
  2. .NET Core SDK 2.2.107
  3. NuGet package
    • NLog 4.6.3
    • NLog.Web.AspNetCore 4.8.2
  4. ASP.NET Core MVC 預設專案範本

前置準備

  1. 註冊 IMemoryCache

    • Startup.cs 中的 ConfigureServices 方法中加入

      services.AddMemoryCache();
      
    • 準備 RegisterPostEvictionCallback

      private void CacheExpireHandler(object key, object value, EvictionReason reason, object state)
      {
          _logger.LogDebug($"Cache Expire ; key :{key};valu:{value};reason:{reason};state:{state}");
          setCache();
      }
      
  2. 註冊 NLog

    • 加入 nlog.config

      <?xml version="1.0" encoding="utf-8" ?>
      <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          autoReload="true"
          internalLogLevel="Info"
          internalLogFile="internal-nlog.txt">
      
          <!-- enable asp.net core layout renderers -->
          <extensions>
              <add assembly="NLog.Web.AspNetCore"/>
          </extensions>
      
          <!-- the targets to write to -->
          <targets>
              <!-- write logs to file  -->
              <target xsi:type="File" name="allfile" fileName="./logs/nlog-all-${shortdate}.log"
                      layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
      
              <!-- another file log, only own logs. Uses some ASP.NET core renderers -->
              <target xsi:type="File" name="ownFile-web" fileName="./logs/nlog-own-${shortdate}.log"
                      layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />
          </targets>
      
          <!-- rules to map from logger name to target -->
          <rules>
              <!--All logs, including from Microsoft-->
              <logger name="*" minlevel="Trace" writeTo="allfile" />
      
              <!--Skip non-critical Microsoft logs and so log only own logs-->
              <logger name="Microsoft.*" maxlevel="Info" final="true" /> <!-- BlackHole without writeTo -->
              <logger name="*" minlevel="Trace" writeTo="ownFile-web" />
          </rules>
      </nlog>
      
    • Program.csCreateWebHostBuilder 加入 .UseNLog();

      public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
          WebHost.CreateDefaultBuilder(args)
                  .UseStartup<Startup>()
                  .UseNLog();
      }
      
  3. 使用 IMemoryCache 與 NLog

    private static IMemoryCache _cache;
    private ILogger<HomeController> _logger;
    
    public HomeController(IMemoryCache cache,ILogger<HomeController> logger)
    {
        _cache = cache;
        _logger = logger;
    }
    

觸發時機

理想狀況就是在 cache expire 當下就觸發 RegisterPostEvictionCallback,即可立馬重新 recache,但實際情況不如預期

  1. 確認觸發時機

    public class HomeController : Controller
    {
        private static IMemoryCache _cache;
        private ILogger<HomeController> _logger;
    
        public HomeController(IMemoryCache cache,ILogger<HomeController> logger)
        {
            _cache = cache;
            _logger = logger;
        }
    
    
        public void SetCache()
        {
            _logger.LogDebug($"before SetCache:{JsonConvert.SerializeObject(_cache)}");
            _logger.LogDebug("SetCache Action");
            setCache();
            _logger.LogDebug($"after SetCache:{JsonConvert.SerializeObject(_cache)}");
    
        }
    
        public IActionResult Index()
        {
            _logger.LogDebug($"GetCache @ {DateTime.Now}");
            _logger.LogDebug($"before getCache:{JsonConvert.SerializeObject(_cache)}");
    
    
            var cacheData = _cache.Get<string>("dateTimeNow");
            _logger.LogDebug($"after getCache:{JsonConvert.SerializeObject(_cache)}");
    
            return View((object) cacheData);
        }
    
        public IActionResult Privacy()
        {
            return View();
        }
            
        private void setCache()
        {
            MemoryCacheEntryOptions cacheExpirationOptions = new MemoryCacheEntryOptions();
            var datTimeNow = DateTime.Now;
            _logger.LogDebug($"SetCache @ {datTimeNow}");
            cacheExpirationOptions.AbsoluteExpiration = datTimeNow.AddSeconds(10);
            cacheExpirationOptions.Priority = CacheItemPriority.High;
            cacheExpirationOptions.RegisterPostEvictionCallback(CacheExpireHandler, this);
            _cache.Set<string>("dateTimeNow", datTimeNow.ToString(), cacheExpirationOptions);
        }
    
        private void CacheExpireHandler(object key, object value, EvictionReason reason, object state)
        {
            _logger.LogDebug($"Cache Expire ; key :{key};valu:{value};reason:{reason};state:{state}");
            setCache();
        }
    
        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
        }
    }
    
  2. 實際結果

    1log

心得

以測試結果來看, cache 的 expire 機制是被動式的:cache 在無人存取時不會主動刪除,在存取時當下檢查 expire 與否,確定 expire 會連帶刪除 cache 內容接著觸發 RegisterPostEvictionCallback

而觸發 RegisterPostEvictionCallback 的 cache 當次存取除了會造成刪除 cache 內容還會直接回傳 cache miss 無法取得預期中的 cache 內容,因此光憑 RegisterPostEvictionCallback 是沒辦法達成絕對的定期 recache

參考資訊

  1. MemoryCache Class
  2. MemoryCache Class
  3. 快取在記憶體中的 ASP.NET Core
  4. Getting started with ASP.NET Core 2