在 ASP.Net 核心上自动重新生成内存缓存
In-Memory Caching with auto-regeneration on ASP.Net Core
我想没有内置的方法来实现:
我有一些缓存数据,需要始终保持最新(间隔几十分钟)。它的生成大约需要 1-2 分钟,因此有时会导致超时请求。
为了性能优化,我把它放入内存缓存,使用Cache.GetOrCreateAsync
,所以我肯定可以在40分钟内快速访问数据。但是缓存过期仍然需要时间。
我想要一种在数据到期前自动刷新数据的机制,这样用户就不会受到此次刷新的影响,并且在刷新期间仍然可以访问 "old data"。
它实际上会添加一个 "pre-expiration" 进程,这将避免数据过期到达其任期。
我觉得这不是默认 IMemoryCache
缓存的功能,但我可能错了?
它存在吗?如果没有,您将如何开发此功能?
我正在考虑使用 PostEvictionCallbacks
,并在 35 分钟后删除一个条目,这将触发更新方法(它涉及 DbContext)。
我是这样解决的:
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 分钟后通过触发 post 逐出方法的取消令牌过期。
此回调会在数据过期之前刷新数据。
请记住,这将使网站保持唤醒状态并使用内存,即使不使用也是如此。
** * 使用定时器更新 * **
以下class注册为单例。传递 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 选项需要注册为单例,默认选项现在是 Scoped(我相信允许更简单的多租户配置)
services.AddDbContext<AppDbContext>(o =>
{
o.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
o.UseSqlServer(connectionString);
},
contextLifetime: ServiceLifetime.Scoped,
optionsLifetime: ServiceLifetime.Singleton);
我想没有内置的方法来实现:
我有一些缓存数据,需要始终保持最新(间隔几十分钟)。它的生成大约需要 1-2 分钟,因此有时会导致超时请求。
为了性能优化,我把它放入内存缓存,使用Cache.GetOrCreateAsync
,所以我肯定可以在40分钟内快速访问数据。但是缓存过期仍然需要时间。
我想要一种在数据到期前自动刷新数据的机制,这样用户就不会受到此次刷新的影响,并且在刷新期间仍然可以访问 "old data"。
它实际上会添加一个 "pre-expiration" 进程,这将避免数据过期到达其任期。
我觉得这不是默认 IMemoryCache
缓存的功能,但我可能错了?
它存在吗?如果没有,您将如何开发此功能?
我正在考虑使用 PostEvictionCallbacks
,并在 35 分钟后删除一个条目,这将触发更新方法(它涉及 DbContext)。
我是这样解决的:
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 分钟后通过触发 post 逐出方法的取消令牌过期。 此回调会在数据过期之前刷新数据。
请记住,这将使网站保持唤醒状态并使用内存,即使不使用也是如此。
** * 使用定时器更新 * **
以下class注册为单例。传递 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 选项需要注册为单例,默认选项现在是 Scoped(我相信允许更简单的多租户配置)
services.AddDbContext<AppDbContext>(o =>
{
o.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
o.UseSqlServer(connectionString);
},
contextLifetime: ServiceLifetime.Scoped,
optionsLifetime: ServiceLifetime.Singleton);