在ASP.Net Core上使用自动重新生成的内存缓存

时间:2017-06-23 13:46:31

标签: caching asp.net-core in-memory

我想没有内置的方法来实现这个目标:

我有一些缓存数据,需要始终保持最新(间隔为几十分钟)。它的生成大约需要1-2分钟,因此有时会导致超时请求。

对于性能优化,我使用Cache.GetOrCreateAsync将其放入内存缓存中,因此我确信可以在40分钟内快速访问数据。但是,缓存过期仍需要时间。

我希望有一种机制可以在数据到期之前自动刷新数据,因此用户不会受到此刷新的影响,并且仍然可以在刷新期间访问“旧数据”。

它实际上会添加一个“过期前”流程,以避免数据到期达到其期限。

我觉得这不是默认IMemoryCache缓存的功能,但我可能错了? 它存在吗?如果没有,您将如何开发此功能?

我正在考虑使用PostEvictionCallbacks,并在35分钟后删除一个条目集,这将触发更新方法(它涉及DbContext)。

1 个答案:

答案 0 :(得分:7)

我就是这样解决的:

Web请求调用的部分(“Create”方法应该仅在第一次调用时)。

var allPlaces = await Cache.GetOrCreateAsync(CACHE_KEY_PLACES
    , (k) =>
    {
       k.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(40);
       UpdateReset();
       return GetAllPlacesFromDb();
    });

然后神奇(这可以通过计时器实现,但不想在那里处理计时器)

// This method adds a trigger to refresh the data from background
private void UpdateReset()
{
    var mo = new MemoryCacheEntryOptions();
    mo.RegisterPostEvictionCallback(RefreshAllPlacessCache_PostEvictionCallback);
    mo.AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(TimeSpan.FromMinutes(35)).Token));
    Cache.Set(CACHE_KEY_PLACES_RESET, DateTime.Now, mo);
}

// Method triggered by the cancellation token that triggers the PostEvictionCallBack
private async void RefreshAllPlacesCache_PostEvictionCallback(object key, object value, EvictionReason reason, object state)
{
    // Regenerate a set of updated data
    var places = await GetLongGeneratingData();
    Cache.Set(CACHE_KEY_PLACES, places, TimeSpan.FromMinutes(40));

    // Re-set the cache to be reloaded in 35min
    UpdateReset();
}

因此缓存获得两个条目,第一个条目包含数据,40分钟后到期,第二个条目在35分钟后通过触发驱逐后驱逐方法的取消标记到期。 此回调在数据到期之前刷新数据。

请记住,即使没有使用,这也会使网站保持清醒并使用内存。

** *使用计时器更新* **

以下类被注册为单身人士。传递DbContextOptions而不是DbContext来创建具有正确范围的DbContext。

public class SearchService
{
    const string CACHE_KEY_ALLPLACES = "ALL_PLACES";
    protected readonly IMemoryCache Cache;
    private readonly DbContextOptions<AppDbContext> AppDbOptions;
    public SearchService(
            DbContextOptions<AppDbContext> appDbOptions,
            IMemoryCache cache)
    {
        this.AppDbOptions = appDbOptions;
        this.Cache = cache;
        InitTimer();
    }
    private void InitTimer()
    {
        Cache.Set<AllEventsResult>(CACHE_KEY_ALLPLACESS, new AllPlacesResult() { Result = new List<SearchPlacesResultItem>(), IsBusy = true });

        Timer = new Timer(TimerTickAsync, null, 1000, RefreshIntervalMinutes * 60 * 1000);
    }
    public Task LoadingTask = Task.CompletedTask;
    public Timer Timer { get; set; }
    public long RefreshIntervalMinutes = 10;
    public bool LoadingBusy = false;

    private async void TimerTickAsync(object state)
    {
        if (LoadingBusy) return;
        try
        {
            LoadingBusy = true;
            LoadingTask = LoadCaches();
            await LoadingTask;
        }
        catch
        {
            // do not crash the app
        }
        finally
        {
            LoadingBusy = false;
        }
    }
    private async Task LoadCaches()
    {
       try
       {
           var places = await GetAllPlacesFromDb();
           Cache.Set<AllPlacesResult>(CACHE_KEY_ALLPLACES, new AllPlacesResult() { Result = places, IsBusy = false });
       }
       catch{}
     }
     private async Task<List<SearchPlacesResultItem>> GetAllPlacesFromDb() 
     {
         // blablabla
     }

 }

注意: DbContext选项需要注册为singleton,默认选项现在是Scoped(我相信允许更简单的多租户配置)

services.AddDbContext<AppDbContext>(o =>
    {
        o.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
        o.UseSqlServer(connectionString);
    }, 
    contextLifetime: ServiceLifetime.Scoped, 
    optionsLifetime: ServiceLifetime.Singleton);