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

来源:这里教程网 时间:2026-02-21 17:36:44 作者:

MemoryCache
配合
GetOrCreateAsync
是最简安全路径

高并发下直接读写缓存(比如先

TryGetValue
Set
)必然引发重复计算和缓存击穿。.NET 6+ 的
MemoryCache.GetOrCreateAsync
内部已用
ConcurrentDictionary
+ 懒初始化锁机制,能确保同一 key 的 factory 只执行一次,其余并发请求自动等待并复用结果。

关键点:

GetOrCreateAsync
的 factory 返回
Task<t></t>
,必须是异步加载逻辑(如调用数据库或 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<string bool></string>
记录是否已处理过某 key 的淘汰
不要在回调里试图重新
Set
同一个 key——此时缓存已空,又没走
GetOrCreateAsync
的协调流程,极易引发竞态
缓存安全的核心不在“加锁”,而在“让框架替你协调”。只要 factory 是纯异步、key 是确定性的、更新动作走
Refresh
,95% 的高并发缓存问题就消失了。剩下那些,往往出在业务逻辑把缓存当数据库用了。

相关推荐