【问题标题】:LazyCache: Regularly refresh cached itemsLazyCache:定期刷新缓存项
【发布时间】:2019-06-02 04:11:00
【问题描述】:

我正在使用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 作为参数。 在预定时间触发重新加载缓存的最佳方式是什么?第一个用户的等待时间最短?

【问题讨论】:

    标签: caching .net-core memorycache lazycache


    【解决方案1】:

    我发现了类似的问题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;
                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;
            }
    
     }
    

    }

    【讨论】:

    • 如果您使用计时器,您可能不需要使用AbsoluteExpirationRelativeToNow 使条目过期。否则,您可能会遇到缓存为空的情况。无论如何,计时器都会为您提供新鲜的内容。
    【解决方案2】:

    现在可以使用 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));
            }
    

    【讨论】:

    • 如果我正确理解 ExpirationMode.ImmediateExpiration 行为,它并不能解决在 GetStuff 过期时间和返回之间的间隙中长时间运行的 GetStuff 方法的延迟。最好有一个可选参数 reloadBeforeExpireTimeSpan ,这样可以避免这种差距。我最近建议类似于 Polly (github.com/App-vNext/Polly/issues/794)。顺便说一句,您添加的模式更合适的名称是 ExpirationMode.ImmediateEviction 而不是 ExpirationMode.ImmediateExpiration
    • 错字的好地方!会解决这个问题
    • 如果您需要在到期前过早地重新填充缓存,那么您需要在自己的线程中运行一些东西,并使用自己的计时器调用缓存上的 add 方法。这可能应该包含在 IHostedService 或类似内容中,以便主机知道它。在github.com/alastairtree/LazyCache/issues/95 中讨论了一下
    猜你喜欢
    • 1970-01-01
    • 2012-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多