LazyCache:定期刷新缓存的项目

时间:2019-06-02 04:11:01

标签: caching .net-core memorycache lazycache

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

公共异步任务>> 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作为参数。 在第一个用户的等待时间最短的情况下,按计划的时间触发缓存重新加载的最佳方法是什么?

2 个答案:

答案 0 :(得分:1)

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

ExpirationMode.ImmediateExpiration

答案 1 :(得分:0)

我发现了类似的问题In-Memory Caching with auto-regeneration on ASP.Net Core,建议将其称为 AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(_options.ReferenceDataRefreshTimeSpan).Token)

我尝试过,但是没有使它起作用。但是,相同的答案通过使用计时器具有替代(推荐)选项。我创建了一个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 https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core
    /// </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;
             _timer =  _cache.GetOrAdd("Timer_for_"+cacheKey, () => CreateTimer(refreshTimeSpan));
            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()
        {
            //Make  Expiration a bit bigger( =1.1) than _refreshTimeSpan, to let refresh before expiry
            TimeSpan expirationTimeSpan = new TimeSpan(Convert.ToInt64(_refreshTimeSpan.Ticks * 1.1));
            var cacheEntryOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = expirationTimeSpan
            };
            //I've attempted to use AddExpirationToken and RegisterPostEvictionCallback, but wasn't able to invoke reliably.
            // Instead timer from https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core seems working
            //.AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(_refreshTimeSpan).Token))
            //.RegisterPostEvictionCallback(
            //    async (key, value, reason, state) =>
            //    {
            //        await GetOrAddAsync(_cacheKey,  _functionToLoad, _refreshTimeSpan);//suppose to save to cache
            //        _logger.LogInformation($" {nameof(T)} are reloaded  ");
            //    });
            return cacheEntryOptions;
        }