MemoryCache.GetOrCreateAsync是最简安全路径,其内置并发协调机制确保同一key的factory只执行一次;应避免手动加锁、慎用Set而改用Refresh,key需纯函数式构造,淘汰回调须幂等。

用 MemoryCache 配合 GetOrCreateAsync 是最简安全路径
高并发下直接读写缓存(比如先 TryGetValue 再 Set)必然引发重复计算和缓存击穿。.NET 6+ 的 MemoryCache.GetOrCreateAsync 内部已用 ConcurrentDictionary + 懒初始化锁机制,能确保同一 key 的 factory 只执行一次,其余并发请求自动等待并复用结果。
关键点:
-
GetOrCreateAsync的 factory 返回Task,必须是异步加载逻辑(如调用数据库或 HTTP API),不能塞同步阻塞操作 - 缓存项过期后,下一次访问仍会触发 factory,但多个并发请求仍被串行化——这是预期行为,不是 bug
- 不要手动在 factory 里加
lock或SemaphoreSlim,这会抵消框架内置的协调能力
var value = await _cache.GetOrCreateAsync("user:123", async entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(10);
return await _userService.GetUserByIdAsync(123); // 真实异步 IO
});
需要主动更新缓存时,用 Refresh 而非 Set
如果业务要求“后台更新数据后立刻刷新缓存”,直接 Set 会覆盖正在被 GetOrCreateAsync 执行中的 factory,导致脏数据或异常。正确做法是调用 Refresh —— 它不改变值,只重置过期计时器,并标记该 entry 为“已刷新”,避免其他线程误判为过期而重复加载。
典型场景:用户资料修改成功后同步刷新缓存
-
Refresh("user:123")安全,不会中断正在进行的GetOrCreateAsync -
Remove("user:123")后再Set是危险的,可能引发瞬间大量并发回源 - 若需强制重载新值(而非仅重置过期),应配合
GetOrCreateAsync的entry.SetOptions重新设置过期策略
自定义缓存键要防哈希冲突和并发竞争
缓存 key 是字符串,但业务中常拼接参数生成,比如 "order:" + orderId + ":summary"。高并发下若 key 生成逻辑含非线程安全状态(如静态 StringBuilder、共享变量),会导致 key 错乱,进而缓存污染或击穿。
- 永远用不可变、纯函数式方式构造 key:
$"order:{orderId}:summary",别用string.Format配共享格式器 - 避免在 key 中嵌入动态时间戳(如
DateTime.Now.ToString("HHmm")),这会让缓存失效加速且无法共享 - 对复杂对象做 key 时,用
HashCode.Combine(a, b, c)生成 int 再转字符串,比JsonSerializer.Serialize(obj)更轻量且确定性更强
警惕 PostEvictionCallbacks 中的并发副作用
注册缓存淘汰回调(RegisterPostEvictionCallback)常用于清理关联资源,但回调执行时机不确定,且可能被多个线程并发触发(尤其当缓存批量清除时)。
- 回调函数内禁止调用可能再次触发缓存读写的代码,否则易形成递归淘汰
- 所有外部操作(如发 MQ、写日志)必须幂等;例如用
ConcurrentDictionary记录是否已处理过某 key 的淘汰 - 不要在回调里试图重新
Set同一个 key——此时缓存已空,又没走GetOrCreateAsync的协调流程,极易引发竞态
Refresh,95% 的高并发缓存问题就消失了。剩下那些,往往出在业务逻辑把缓存当数据库用了。










