You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 15:36:03