c# 如何在高并发下安全地更新缓存

15次阅读

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

c# 如何在高并发下安全地更新缓存

MemoryCache 配合 GetOrCreateAsync 是最简安全路径

高并发下直接读写缓存(比如先 TryGetValueSet)必然引发重复计算和缓存击穿。.net 6+ 的 MemoryCache.GetOrCreateAsync 内部已用 ConcurrentDictionary + 懒初始化锁机制,能确保同一 key 的 factory 只执行一次,其余并发请求自动等待并复用结果。

关键点:

  • GetOrCreateAsync 的 factory 返回 Task,必须是异步加载逻辑(如调用数据库http API),不能塞同步阻塞操作
  • 缓存项过期后,下一次访问仍会触发 factory,但多个并发请求仍被串行化——这是预期行为,不是 bug
  • 不要手动在 factory 里加 lockSemaphoreSlim,这会抵消框架内置的协调能力
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 是危险的,可能引发瞬间大量并发回源
  • 若需强制重载新值(而非仅重置过期),应配合 GetOrCreateAsyncentry.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 的协调流程,极易引发竞态

缓存安全的核心不在“加锁”,而在“让框架替你协调”。只要 factory 是纯异步、key 是确定性的、更新动作走 Refresh,95% 的高并发缓存问题就消失了。剩下那些,往往出在业务逻辑把缓存当数据库用了。

text=ZqhQzanResources