先说结论
穿透是查询本就不存在的数据,缓存和数据库都被反复访问;击穿是单个热点 Key 失效后大量请求同时回源;雪崩是大量 Key 同时失效或缓存整体不可用,引发回源洪峰。三者流量形态不同,治理手段也不同。
穿透可做参数校验、缓存空值或布隆过滤;击穿可用互斥重建、逻辑过期和热点预热;雪崩要让过期时间离散化,并配合高可用、限流、熔断和降级。
方案细节
空值缓存必须设置短 TTL,避免无效数据长期占用。布隆过滤器可能误判存在但不会误判不存在,且新增数据要同步更新。互斥重建的锁要有超时、唯一标识和失败兜底。
三类问题的流量图景
| 问题 | 触发条件 | 回源范围 | 主要风险 | 优先手段 |
|---|---|---|---|---|
| 穿透 | 恶意或错误查询不存在 ID | 同一类无效请求 | 数据库被无效查询打穿 | 参数校验、布隆过滤、空值缓存 |
| 击穿 | 单个热点 Key 刚过期 | 一个热点数据 | 大量并发同时重建 | 请求合并、互斥、逻辑过期 |
| 雪崩 | 大量 Key 同时失效或 Redis 故障 | 大范围数据 | 数据库与应用整体过载 | TTL 打散、高可用、限流降级 |
先用监控确认是哪一种:若大量请求集中在不存在的 ID,是穿透;只有某热门商品在 TTL 到点后抖动,是击穿;全站命中率同时下降、回源与连接池激增,才接近雪崩。错误分类会导致错误治理,例如给穿透加分布式锁只会制造更多锁竞争。
穿透的分层防线
边缘参数校验 -> 业务鉴权/限速 -> 布隆过滤器 -> 空值缓存 -> 数据库
布隆过滤器保存“可能存在”的集合。查询结果为不存在时可以直接拒绝,结果为存在仍需查缓存和数据库,因为会有假阳性。过滤器容量、hash 数量和误判率需要基于预计元素量计算;新增、删除和重建时的更新策略必须明确,不能把它当永远正确的数据库镜像。
空值缓存适合数据确实不存在或短时间不会创建的场景:
User user = repository.find(id);
redis.setex(key(id), user == null ? NULL_MARKER : toJson(user),
user == null ? 60 : 3600 + randomJitter());
若“查询不存在后马上创建”是常见业务,空值 TTL 太长会造成读到假不存在,需要在创建成功时主动删除或更新缓存。
击穿的三种方案
互斥重建让第一个请求获取短锁回源,其他请求等待、短暂重试或返回降级;优点是数据库压力低,缺点是高峰请求会增加等待。请求合并可以在单实例把同 Key 的并发请求汇聚为一个 Future,减少锁和 Redis 往返,但跨实例仍需协作。
逻辑过期把“物理 TTL”与“业务新鲜度”分开:缓存一直保留,逻辑过期后一个后台任务刷新,其他请求暂时读旧值。它适合允许短暂陈旧的商品详情、配置等场景,但必须设置最大陈旧期限、刷新超时、失败告警和最终兜底,不能让旧数据无限存在。
雪崩下的容量保护
随机 TTL 只能分散正常到期,无法处理 Redis 集群整体故障或代码发布导致的批量删 Key。系统还需要:
- Redis 主从/Cluster 与跨故障域部署,明确 RTO/RPO。
- 本地只读缓存或静态降级页,减少完全失效时的回源。
- 网关与服务层限流,按数据库可承受 QPS 而不是入口 QPS 设置阈值。
- 数据库连接池有界、查询超时和熔断,避免请求无限堆积。
- 预热关键数据,并在发布、扩容、故障切换时演练命中率下降。
互斥重建的安全边界
锁值必须唯一,释放时通过 Lua 比较值后删除,TTL 要覆盖正常重建时间但不能无限长。若重建任务超时,后续请求可能接管;写入缓存时需用版本或时间判断,避免旧任务晚完成覆盖新数据。热点刷新失败时要区分“可读旧值”和“必须失败”,由业务新鲜度决定。
防护层次
应用本地缓存可降低 Redis 故障冲击,但要接受短暂陈旧。数据库连接池、请求队列和限流阈值应按数据库可承受能力设置,而不是按入口峰值设置。
容易踩坑的地方
三类问题不能只靠“加分布式锁”统一解决。锁本身也可能成为热点;缓存高可用只能降低整体宕机概率,不能解决同一时刻大量业务 Key 到期。
常见问题
追问:逻辑过期如何避免返回永久旧数据?
缓存值携带逻辑过期时间,过期后一个请求异步重建,其他请求暂时读旧值;同时要有最大陈旧时间、重建失败告警和主动兜底。
追问:布隆过滤器会不会漏掉真实数据?
正确构建时不会产生假阴性,但数据新增未同步、过滤器损坏或多版本切换错误会导致实际漏判,因此更新链路和重建策略同样重要。
追问:热点 Key 到期时为什么不让所有请求直接查库?
瞬时并发会把一个本来很轻的单行查询放大到数千次,挤占连接池和 CPU,还可能级联触发更多超时重试。应该合并重建工作而不是放大它。
追问:缓存雪崩时应该先扩数据库吗?
扩容可能帮助,但若回源请求无上限,新容量也会被打满。应先限流、降级、保护连接池并恢复缓存命中,再根据容量曲线扩展资源。