缓存穿透、击穿与雪崩
用途:解释“缓存突然失效导致数据库被打挂”这类故障,并给出对策
三种问题
「三种问题」小节链接| 问题 | 定义 | 典型征兆 |
|---|---|---|
| 穿透 | 查询根本不存在的数据,缓存无值,每次打到 DB | DB QPS 高但命中率低;key 分布异常(大量不存在的 ID) |
| 击穿 | 热点 key 过期瞬间大量请求同时回源 | 单点毛刺,DB 瞬时并发尖峰 |
| 雪崩 | 大批 key 同时过期或缓存实例整体不可用 | 周期性尖峰,或缓存故障后 DB 崩溃 |
穿透对策
「穿透对策」小节链接- 空值缓存:查不到也写
NULL占位(短 TTL,如 1-5 分钟)。实现简单,代价是内存与“新数据插入后的窗口不一致”。 - 布隆过滤器:前置判断 key 是否可能存在(RedisBloom 或本地 Guava/ScalableBloomFilter)。注意:不支持删除(需用计数型/分区方案),存在假阳性。
- 输入校验:对明显非法的参数(格式、范围、负数 ID)在入口拦截。
- 接口限流:对未知 key 的高频访问做单来源限流,防刷。
击穿对策
「击穿对策」小节链接- 互斥回源:只允许一个线程回源,其余等待或返回旧值。实现:
SET key_lock NX EX,或本地 singleflight。 - 逻辑过期:value 内嵌过期时间,读到“逻辑过期”时异步刷新、同步返回旧值。彻底避免回源风暴,代价是短暂脏读。
- 热点常驻:真热点不设 TTL,由后台任务定期刷新(配合变更事件失效)。
- 永不过期 + 主动失效:写入路径负责删除/更新缓存,读路径不过期。
雪崩对策
「雪崩对策」小节链接- TTL 加随机抖动:
ttl = base + rand(0, 0.2*base),避免同刻集中失效。 - 多级缓存:本地缓存(Caffeine)+ 分布式缓存,本地层吸收大部分瞬时并发。
- 缓存高可用:哨兵/Cluster、跨可用区部署、客户端熔断与降级(缓存不可用时限流回源而不是全量回源)。
- 预热:发布或重启后按访问频率预加载热点。
- DB 侧兜底:连接池上限 + 慢查询熔断 + 读接口降级(返回近似数据或缓存旧值)。
统一的防线:回源限流
「统一的防线:回源限流」小节链接无论哪种问题,最后一道防线都是限制回源并发。做法:信号量/令牌桶限制“正在查 DB 的请求数”,超出的请求直接返回降级结果。这比让 DB 排队更可控——数据库的排队能力是它最差的资源。
常见误区
「常见误区」小节链接| 误区 | 修正 |
|---|---|
用 SETNX 锁但忘记释放或过期时间 |
锁必须带 TTL,否则死锁;释放要判断持有者(值用唯一 token) |
| 空值缓存 TTL 与正常值一样长 | 空值应更短,避免新数据长期不可见 |
| 布隆过滤器当权威判据 | 假阳性会放行部分穿透请求,仍需 DB 兜底 |
| 所有 key 设同一个整点过期 | 制造雪崩;TTL 必须打散 |
| 只加缓存不做容量与故障演练 | 缓存故障时的 DB 承载能力必须实测 |
延伸阅读
「延伸阅读」小节链接- Martin Kleppmann《数据密集型应用系统设计》第 1 章(缓存一致性与失效)
- Caffeine、RedisBloom 官方文档