Asp.Net Core中实现单例CacheManager及缓存自动重填方案咨询
现有方案评估
你当前的注册实现可以满足基础的启动时缓存加载需求,但存在两个可优化点:
IMemoryCache本身就是Singleton生命周期,不需要从创建的子Scope中获取,直接从根服务提供者sp获取即可,避免不必要的Scope资源消耗。- 把缓存初始化逻辑写在服务注册的工厂方法中,会导致服务注册和业务初始化逻辑耦合,后续要加重试、异常处理等逻辑时会让注册代码过于臃肿,更推荐把初始化逻辑拆分到
IHostedService中实现,职责更清晰。
缓存自动重填实现
要实现缓存失效后自动重填,需要用到IMemoryCache内置的驱逐回调(PostEvictionCallback),同时因为重填时需要调用Scoped生命周期的ILookupService,需要给CacheManager注入IServiceScopeFactory来动态创建Scope获取服务,完整实现如下:
改造CacheManager
public interface ICacheManager { void InitLoadCache(); MyData GetCacheData(); } public class CacheManager: ICacheManager { private readonly IMemoryCache _cache; private readonly IServiceScopeFactory _scopeFactory; private const string MyDataCacheKey = "MyData_Cache_Key"; // 缓存过期时间可按需调整 private readonly TimeSpan _cacheExpiration = TimeSpan.FromHours(2); public CacheManager(IMemoryCache cache, IServiceScopeFactory scopeFactory) { _cache = cache; _scopeFactory = scopeFactory; } // 内部缓存加载/重填核心逻辑 private async Task<MyData> LoadCacheFromDbAsync() { // 动态创建Scope获取Scoped服务 using var scope = _scopeFactory.CreateScope(); var lookupService = scope.ServiceProvider.GetRequiredService<ILookupService>(); var dbData = await lookupService.GetDataAsync(); // 配置缓存规则+驱逐回调 var cacheOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = _cacheExpiration, Priority = CacheItemPriority.High // 降低被系统主动回收的概率 }; cacheOptions.RegisterPostEvictionCallback(OnCacheEvicted); _cache.Set(MyDataCacheKey, dbData, cacheOptions); return dbData; } // 缓存驱逐回调 private async void OnCacheEvicted(object key, object value, EvictionReason reason, object state) { // 手动移除、主动替换的缓存不触发重填,只在过期/容量回收时重填 if (reason is EvictionReason.Removed or EvictionReason.Replaced) return; try { await LoadCacheFromDbAsync(); } catch (Exception ex) { // 此处可添加日志记录、延时重试逻辑,避免异常扩散 } } // 对外暴露的启动初始化方法 public async void InitLoadCache() { await LoadCacheFromDbAsync(); } // 对外暴露的缓存获取方法,兜底处理缓存为空的异常情况 public MyData GetCacheData() { if (_cache.TryGetValue<MyData>(MyDataCacheKey, out var cacheData)) return cacheData; // 缓存为空时同步加载兜底,避免缓存雪崩 return LoadCacheFromDbAsync().GetAwaiter().GetResult(); } }
调整服务注册逻辑
不需要在工厂方法中写复杂初始化逻辑,直接注册服务即可,同时添加托管服务实现启动时自动加载缓存:
public void ConfigureServices(IServiceCollection services) { // EF上下文注册 services.AddDbContext<MyDatabaseContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); // Scoped业务服务注册 services.AddScoped<ILookupService, LookupService>(); services.AddMemoryCache(); // 注册CacheManager单例 services.AddSingleton<ICacheManager, CacheManager>(); // 注册托管服务,启动时自动执行缓存初始化 services.AddHostedService<CacheInitService>(); }
实现启动初始化托管服务
public class CacheInitService : IHostedService { private readonly ICacheManager _cacheManager; public CacheInitService(ICacheManager cacheManager) { _cacheManager = cacheManager; } public Task StartAsync(CancellationToken cancellationToken) { _cacheManager.InitLoadCache(); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
注意事项
- 驱逐回调标记为
async void是因为PostEvictionCallback原生不支持异步返回值,一定要做好内部异常捕获,避免未处理异常导致进程崩溃。 - 高并发场景下可以加本地锁,避免缓存失效时多个线程同时触发重填请求压垮数据库。
- 如果是分布式部署场景,可以额外添加分布式锁,避免多个节点同时重填相同缓存。
内容的提问来源于stack exchange,提问作者LP13
相关产品推荐
相关产品推荐

