LazyCache:定期刷新缓存项

LazyCache: Regularly refresh cached items

我正在使用 LazyCache 并希望刷新缓存,例如每小时,但理想情况下,我希望缓存项过期后的第一个调用者不要等待缓存重新加载。我写了以下内容

public async Task<List<KeyValuePair<string, string>>> GetCarriersAsync()
{

    var options = new MemoryCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = new TimeSpan(1,0,0),// consider to config
    }.RegisterPostEvictionCallback(
         async  (key, value, reason, state) =>
        {
            await GetCarriersAsync();//will save to cache
            _logger.LogInformation("Carriers are reloaded: " );
        });
    Func<Task<List<KeyValuePair<string, string>>>> cacheableAsyncFunc = () => GetCarriersFromApi();
    var cachedCarriers = await _cache.GetOrAddAsync($"Carriers", cacheableAsyncFunc, options);

    return cachedCarriers;
}

然而,当缓存项过期时不会调用 RegisterPostEvictionCallback,而只会在下一次对该项的请求发生时调用(并且调用者需要等待冗长的操作)。

线程 Expiration almost never happens on it's own in the background #248 解释说 这是设计使然,建议使用变通方法指定 CancellationTokenSource.CancelAfter(TimeSpan.FromHours(1)) 而不是 SetAbsoluteExpiration。

很遗憾 LazyCache.GetOrAddAsync 没有 CancellationToken 作为参数。 在预定时间触发缓存重新加载且第一位用户的等待时间最短的最佳方法是什么?

我发现了类似的问题 建议调用 AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(_options.ReferenceDataRefreshTimeSpan).Token).

我试过了,但没成功。但是,通过使用计时器,相同的答案有替代(和推荐)选项。我创建了一个 class RefreshebleCache,用于不同的可缓存选项,如下所示:

   var refreshebleCache = new RefreshebleCache<MyCashableObjectType>(_cache, _logger);
   Task<MyCashableObjectType> CacheableAsyncFunc() => GetMyCashableObjectTypeFromApiAsync();
   var cachedResponse = await refreshebleCache.GetOrAddAsync("MyCashableObject", CacheableAsyncFunc,
                        _options.RefreshTimeSpan);

RefreshebleCache 实现:

/// <summary>
    /// Based on 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class RefreshebleCache<T>
    {

        protected readonly IAppCache _cache;
        private readonly ILogger _logger;
        public bool LoadingBusy = false;
        private string _cacheKey;
        private TimeSpan _refreshTimeSpan;
        private Func<Task<T>> _functionToLoad;
        private Timer _timer;

        public RefreshebleCache(IAppCache cache, ILogger logger)
        {

            _cache = cache;
            _logger = logger;
        }

        public async Task<T>  GetOrAddAsync (string cacheKey , Func<Task<T>> functionToLoad, TimeSpan refreshTimeSpan)
        {
            _refreshTimeSpan= refreshTimeSpan;
            _functionToLoad = functionToLoad;
            _cacheKey = cacheKey;
            var timerCachedKey = "Timer_for_"+cacheKey;
            //if removed from cache, _timer could continue to work, creating redundant calls
            _timer =  _appCache.GetOrAdd(timerCachedKey, () => 
             CreateTimer(refreshTimeSpan), 
  SetMemoryCacheEntryOptions(CacheItemPriority.NeverRemove));
            var cachedValue = await LoadCacheEntryAsync();
            return  cachedValue;
        }
        private Timer CreateTimer(TimeSpan refreshTimeSpan)
        {
            Debug.WriteLine($"calling CreateTimer for {_cacheKey} refreshTimeSpan {refreshTimeSpan}"); //start first time in refreshTimeSpan
            return new Timer(TimerTickAsync, null, refreshTimeSpan, refreshTimeSpan);
        }

    
        private async void TimerTickAsync(object state)
        {
            if (LoadingBusy) return;
            try
            {
                LoadingBusy = true;
                Debug.WriteLine($"calling LoadCacheEntryAsync from TimerTickAsync for {_cacheKey}");
                var loadingTask = LoadCacheEntryAsync(true);
                await loadingTask;
            }
            catch(Exception e)
            {
                _logger.LogWarning($" {nameof(T)} for {_cacheKey} was not reloaded.    {e} ");
            }
            finally
            {
                LoadingBusy = false;
            }
        }
        private async Task<T> LoadCacheEntryAsync(bool update=false)
        {
            var cacheEntryOptions = SetMemoryCacheEntryOptions();

            Func<Task<T>> cacheableAsyncFunc = () => _functionToLoad();
            Debug.WriteLine($"called LoadCacheEntryAsync for {_cacheKey} update:{update}");
            T cachedValues = default(T);
            if (update)
            {
                cachedValues =await cacheableAsyncFunc();
                if (cachedValues != null)
                {
                    _cache.Add(_cacheKey, cachedValues, cacheEntryOptions);
                }

                //    _cache.Add(_cacheKey, cacheableAsyncFunc, cacheEntryOptions);
            }
            else
            {
                 cachedValues = await _cache.GetOrAddAsync(_cacheKey, cacheableAsyncFunc, cacheEntryOptions);
            }
            return cachedValues;
        }
        private MemoryCacheEntryOptions SetMemoryCacheEntryOptions(CacheItemPriority priority= CacheItemPriority.Normal)
       {
          var cacheEntryOptions = new MemoryCacheEntryOptions
          {
            Priority = priority 
          };
          return cacheEntryOptions;
        }

 }

}

现在可以使用 LazyCache 2.1 实现自动刷新,使用 LazyCacheEntryOptionsExpirationMode.ImmediateExpiration 它们实际上只是时间延迟取消令牌的包装器。您可以在以下取自 LazyCache 测试套件的测试中看到这一点:

        [Test]
        public async Task AutoRefresh()
        {
            var key = "someKey";
            var refreshInterval = TimeSpan.FromSeconds(1);
            var timesGenerated = 0;

            // this is the Func what we are caching 
            ComplexTestObject GetStuff()
            {
                timesGenerated++;
                return new ComplexTestObject();
            }

            // this sets up options that will recreate the entry on eviction
            MemoryCacheEntryOptions GetOptions()
            {
                var options = new LazyCacheEntryOptions()
                    .SetAbsoluteExpiration(refreshInterval, ExpirationMode.ImmediateExpiration);
                options.RegisterPostEvictionCallback((keyEvicted, value, reason, state) =>
                {
                    if (reason == EvictionReason.Expired  || reason == EvictionReason.TokenExpired)
                        sut.GetOrAdd(key, _ => GetStuff(), GetOptions());
                });
                return options;
            }

            // get from the cache every 2s
            for (var i = 0; i < 3; i++)
            {
                var thing = sut.GetOrAdd(key, () => GetStuff(), GetOptions());
                Assert.That(thing, Is.Not.Null);
                await Task.Delay(2 * refreshInterval);
            }

            // we refreshed every second in 6 seconds so generated 6 times
            // even though we only fetched it every other second which would be 3 times
            Assert.That(timesGenerated, Is.EqualTo(6));
        }