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

为什么不能直接 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 会再次抛出。这通常不是期望行为:
ShopNC多用户商城,全新的框架体系,呈现给您不同于以往的操作模式,更简约的界面,更流畅的搜索机制,更具人性化的管理后台操作,更适应现在网络的运营模式解决方案,为您的创业之路打下了坚实的基础,你们的需求就是我们的动力。我们在原有的C-C模式的基础上更增添了时下最流行的团购频道,进一步的为您提高用户的活跃度以及黏性提供帮助。ShopNC商城系统V2.4版本新增功能及修改功能如下:微商城频道A、商城
- 可在
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 不被多次触发或错误重用。









