缓存键怎么设计
键要能唯一定位一份数据,且随依赖变化。常见错误是键里漏了版本或租户,导致串数据。
TTL 不是越大越好
TTL 越长越可能读到旧值;越短命中率越低。按数据可变频率设:配置类可长,库存类要短或主动失效。
三种典型故障
| 问题 | 现象 | 对策 |
|---|---|---|
| 穿透 | 查不存在的 key 打到库 | 缓存空值 / 布隆过滤器 |
| 击穿 | 热点 key 失效瞬间洪峰 | 互斥重建 / 逻辑过期 |
| 雪崩 | 大量 key 同时失效 | TTL 加随机抖动 |
更新还是删除
写后优先删除缓存(lazy 重建),比立即写回更简单、不易不一致。
实战案例:三次“缓存惹的祸”
- 改价后仍显示旧价:写库成功但缓存没失效;并发下“先删缓存再写库”与“先写库再删缓存”都可能读到旧值。稳妥做法是先写库、后删缓存,并对热点读加短暂互斥。
- 缓存空值把故障盖住:为防止穿透把空结果缓存 10 分钟,结果上游修好后用户仍看到“无数据”。给空值设更短 TTL,或提供手动清理入口。
- 多级缓存各说各话:本地缓存(进程内)与 Redis 不同步,同一用户刷新两次结果不同。给本地缓存设极短 TTL,失效消息广播,或干脆只保留一级。
常见问题(FAQ)
为什么不能先删缓存再写库?删完后、写库完成前的读请求会把旧值重新加载进缓存,形成长期脏数据。TTL 加随机抖动是为什么?避免大量 key 在同一秒集体过期造成雪崩。缓存该不该存大对象?不宜,序列化与网络开销大,且容易拖慢单次请求;大对象宜分片或另存对象存储。怎么验证失效逻辑?写一条自动化用例:写入后立即读,断言读到新值;再做并发压测观察是否回退。
把失效做成可验证的流程
缓存问题的根源往往不是技术选型,而是缺少一套可验证的失效流程。以下做法能在实践中显著降低脏数据与雪崩的概率。
- 明确每一份数据的权威来源:先确定哪个系统是唯一事实来源,缓存只是它的投影。一旦出现两个都可写的存储,一致性就只能靠运气。写入必须只走权威来源,缓存仅被动更新。
- 键的命名要能“批量失效”:设计键时按“业务对象 + 维度 + 版本”组织,例如把租户与对象类型放在前缀里。这样需要清理某类数据时可以按前缀批量删除,而不必逐个枚举键——生产环境里无法批量失效是缓存事故的常见放大器。
- 失效失败要能被发现:删除缓存的调用可能因网络抖动而失败。应记录失败次数并在超过阈值时告警,同时准备兜底手段,例如提高该键的读取频率或临时把读流量绕过缓存。
- 为热点键单独设策略:访问量极高的键一旦过期,重建压力会集中打到数据库。可以对这类键采用逻辑过期,即缓存中保留过期标记,由一个并发受限的协程负责重建,其余请求先拿到旧值。
- 压测失效路径:发布前模拟“大量键同时失效”和“数据库短暂不可用”两种场景,确认系统仍能提供降级内容而不是直接报错。这条验证常常能提前发现最严重的问题。
与业务语义对齐
技术上的失效策略还要与业务可接受的陈旧程度对齐。商品库存通常要求秒级一致,而文章正文可以容忍分钟级延迟。把这些要求明确写进接口说明,缓存时长就不再是凭感觉设定的数字,而是有业务依据的约定。