应优先使用 idistributedcache(如 redis)作为主缓存层,memorycache 仅作二级本地缓存;键需含环境、租户、版本等上下文;失效须主动删除+广播+过期兜底;须防控穿透、击穿、雪崩。

缓存该用 MemoryCache 还是 IDistributedCache
单机部署时用 MemoryCache 足够快,但高并发网站通常多实例部署,各节点内存不共享。此时若只依赖 MemoryCache,同一数据在不同节点可能缓存不同值,甚至一个节点更新了缓存,其他节点仍读旧值——这就是典型的缓存不一致问题。
必须用 IDistributedCache 作为主缓存层(如 Redis 或 SQL Server 后端),MemoryCache 只能作为二级本地缓存(即“缓存穿透防护 + 热点加速”用途)。
- Redis 实现的
IDistributedCache是首选:支持原子操作、过期策略、发布/订阅失效通知 - 避免用 SQL Server 做分布式缓存后端——写入延迟高、连接压力大、不支持批量删除
- 本地
MemoryCache缓存时间建议 ≤ 10 秒,且仅用于高频读、低更新频率的热点数据(如配置项、地区字典)
缓存键设计要防冲突、可预测、带业务上下文
缓存键不是随便拼个字符串就行。常见错误是直接用 "/api/user/" + userId 当键,结果不同环境(测试/生产)、不同租户、不同 API 版本全挤在一个键空间里,导致误击或污染。
推荐格式:"{env}:{tenant}:{version}:{resource}:{id}",例如 "prod:acme:v2:user:12345"。
- 必须包含环境标识(
env),避免测试缓存污染生产 - 多租户系统必须含
tenant,否则 A 公司数据可能被 B 公司接口读到 - API 版本变更时,键自动隔离,无需手动清理旧版本缓存
- 禁止在键中拼接未转义的用户输入(如用户名),防止注入或键超长;用
Convert.ToBase64String(Encoding.UTF8.GetBytes(...))安全编码
失效机制不能只靠自然过期,得有主动踢出 + 失效广播
纯依赖 absoluteExpiration 或 slidingExpiration 极其危险:数据已更新,但缓存还剩 30 秒才过期,这期间所有请求都读脏数据。
必须组合三种方式:
-
写时主动删除:更新 DB 后立刻调用
_cache.RemoveAsync("key")(注意:不是SetAsync覆盖,删除更安全) -
关键路径加失效广播:用 Redis Pub/Sub 或自建消息队列,当订单状态更新时发消息
"order:status:updated:12345",所有节点监听并删对应缓存 - 过期时间设为“上限”而非“默认”:比如商品价格缓存设 5 分钟过期,但每次更新都主动删;过期只是兜底,不是主力机制
特别注意:不要在事务内调用缓存删除——如果事务回滚,缓存却已删,会导致短暂不一致。应在事务成功提交后再删缓存(可用 Transaction.CommitAsync 后挂回调,或投递到可靠消息队列)。
应对缓存穿透、雪崩、击穿的实操要点
高并发下这三个问题不是理论风险,是真实会炸服务的故障点。
-
穿透:查不存在的
userId = -1或随机 ID,缓存没命中,DB 每次都扛压。解决:对空结果也缓存(SetAsync("user:-1", null, new TimeSpan(0, 0, 30))),但需加前缀区分(如"empty:user:-1"),避免和正常 null 冲突 -
击穿:热点 key(如首页 banner)过期瞬间大量请求打穿缓存。解决:用
GetOrCreateAsync+lock或 Redis 的SETNX保证只有一个线程回源加载,其余等待 -
雪崩:大量 key 设相同过期时间(如整点刷新),到期后集体失效。解决:过期时间加随机偏移(
TimeSpan.FromMinutes(5 + new Random().NextDouble() * 2)),且关键数据改用永不过期 + 主动更新
最易被忽略的是:缓存客户端本身也是资源瓶颈。别让 IDistributedCache 的连接池耗尽——Redis 连接字符串必须配 connectTimeout=5000、syncTimeout=2000、abortConnect=false,并启用连接池复用。










