c# 如何实现异步的caching模式

12次阅读

MemoryCache.GetOrCreateAsync并非真异步,其底层为同步阻塞;应缓存Task并用SemaphoreSlim实现并发控制,避免缓存击穿,同时注意异常传播与过期策略。

c# 如何实现异步的caching模式

为什么不能直接 await 一个 MemoryCache.GetOrCreateAsync

因为 MemoryCache 原生不提供真正的异步 API。它的 GetOrCreateAsync 方法只是同步执行缓存逻辑后返回 Task.FromResult,底层仍是阻塞式调用。如果你在回调里写了 await HttpClient.GetAsync(...),整个缓存委托会阻塞线程,违背异步初衷。

用 SemaphoreSlim 控制并发,避免缓存击穿

多个请求同时发现缓存缺失时,应只让一个去加载数据,其余等待结果。直接用 lock 会阻塞线程,改用 SemaphoreSlim 实现异步等待:

private static readonly ConcurrentDictionary _semaphores = new(); private static readonly MemoryCache _cache = new(new MemoryCacheOptions());  public async Task GetOrLoadAsync(string key, Func> factory, TimeSpan? expiration = null) {     var cacheEntry = _cache.Get>(key);     if (cacheEntry != null) return await cacheEntry;      var semaphore = _semaphores.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));     try     {         await semaphore.WaitAsync();         cacheEntry = _cache.Get>(key);         if (cacheEntry != null) return await cacheEntry;          var task = factory(default).AsTask(); // 或直接 factory(CancellationToken.None)         _cache.Set(key, task, expiration ?? TimeSpan.FromMinutes(10));         return await task;     }     finally     {         semaphore.Release();         if (semaphore.CurrentCount == 0) _semaphores.TryRemove(key, out _);     } }

注意缓存项的生命周期和异常传播

Task 对象本身可被缓存,但需留意:如果工厂方法抛出异常,该异常会被包裹进 Task 并缓存——后续调用 await 会再次抛出。这通常不是期望行为:

  • 可在 factory 内部捕获异常,返回默认值或空结果
  • 或使用 TryGet + Task.WhenAny 配合超时控制
  • 避免把 Task 当作“值”缓存后又反复 await ——它不会重放,但异常状态会持续存在

替代方案:用 Lazy> 简化逻辑

若不需要过期策略,ConcurrentDictionary + Lazy> 更轻量:

private static readonly ConcurrentDictionary>> _lazyCache = new();  public Task GetOrLoadLazy(string key, Func> factory)     => _lazyCache.GetOrAdd(key, _ => new Lazy>(factory)).Value;

它天然保证只执行一次工厂函数,且支持异步;缺点是无法主动过期或内存回收,适合短生命周期或低更新频率场景。

真正难处理的是缓存过期 + 异步加载 + 并发安全三者叠加。多数人忽略的是:缓存项不该是 T,而应是 Task,且必须确保这个 Task 不被多次触发或错误重用。

text=ZqhQzanResources