作为高并发架构的标配,Redis 一旦遭遇缓存雪崩、缓存击穿、缓存穿透这三类典型故障,后端数据库会瞬间被打垮,引发全站雪崩。本文结合一线排障经验,逐一拆解三类故障的成因、定位手段与防护方案,并给出可直接落地的代码与检查清单。
一、缓存雪崩:大量 key 在同一时刻集体失效
1.1 故障成因
缓存雪崩指的是在某一时刻,大量缓存 key 同时过期,或者 Redis 实例整体宕机,导致所有请求直接打到数据库。数据库瞬时 QPS 暴涨数倍甚至数十倍,往往扛不住直接被打死,进而引发连锁反应,整个服务雪崩。最常见的诱因是「批量数据初始化时设置了相同的 TTL」,或缓存服务重启后未预热。
1.2 如何快速定位
- 监控 Redis 命中率(hit rate)在故障时间点骤降;
- 数据库 QPS、CPU 在同时段突增,出现慢查询告警;
- 应用日志大量出现
key not found,随后 DB 连接池被打满。
配合 Redis 缓存设计规范 中的监控埋点,可以更快区分「集体失效」与「服务宕机」。当穿透把压力传导到数据库侧时,也可参考 PostgreSQL 慢查询优化 的定位思路反向排查。
1.3 防护:错峰过期 + 多级兜底
核心思路是让 key 的过期时间错开,并对底层加一层保护。给基础 TTL 叠加随机抖动,避免集体失效:
import random
def set_cache_with_jitter(r, key, value, base_ttl=3600, jitter=300):
# base_ttl 基础上叠加 ±jitter 的随机过期,避免集体失效
ttl = base_ttl + random.randint(-jitter, jitter)
r.setex(key, ttl, value)
# 基础 1 小时 + 最多 ±5 分钟抖动,批量写入时过期时间被打散
set_cache_with_jitter(redis_client, "product:1001", data)
此外,接入本地内存(如 Caffeine)作为 L1、Redis 作为 L2 的多级缓存,可在 Redis 抖动时兜底;并配合熔断(如 Sentinel / Resilience4j)在数据库压力过大时快速失败,保护 DB 不被拖垮。
二、缓存击穿:单个热点 key 失效的瞬间
2.1 与雪崩的本质区别
缓存击穿不是「大量 key」,而是某一个被高并发访问的热点 key 在过期瞬间,成百上千的请求同时发现缓存没了,一起穿透到数据库去重建缓存。单 key 的瞬时冲击足以拖垮 DB。典型场景:爆款商品详情、首页热点配置、秒杀库存。
2.2 方案 A:互斥锁重建(SingleFlight 思路)
只允许一个线程去查库并重建缓存,其余请求等待或返回旧值。基于 Redis SET key value NX 实现分布式锁:
# 尝试获取重建锁,10 秒自动释放,避免死锁
SET cache:lock:product:1001 1 NX EX 10
# 拿到锁的线程查库并写回缓存,随后释放锁
SET product:1001 "{...}" EX 3600
DEL cache:lock:product:1001
Java 中更推荐用 Redisson 的 RLock 或 Guava Striped + synchronized,避免自己处理锁续期与误删。Go 可直接用 singleflight 包,天然解决并发重建。
2.3 方案 B:逻辑过期(永不过期 + 异步刷新)
不给 key 设物理 TTL,而是在 value 中存入逻辑过期时间;发现逻辑过期时提交异步线程重建,当前请求直接返回旧值。优势是无锁、响应快,代价是可能短暂返回旧数据,适合容忍弱一致性的场景。
三、缓存穿透:查的是根本不存在的数据
3.1 成因与危害
缓存穿透指查询一个数据库中也不存在的 key(如恶意刷 id=-1、或越权遍历)。因为查不到,缓存永远不会被写入,每次请求都穿透到 DB。若遭遇脚本批量扫库,DB 会被活活拖死,且常规限流往往拦不住这种「合法但无效」的查询。
3.2 防护一:缓存空值
对查不到的结果也写入一个短期空值(如 NULL 或特殊标记),并设置较短 TTL,挡住重复查询:
public String getWithNilGuard(String key) {
String val = redis.get(key);
if (val != null) {
return "NIL".equals(val) ? null : val; // 命中空值标记,直接拦截
}
val = db.query(key); // 查库
if (val == null) {
redis.setex(key, 60, "NIL"); // 缓存空值,60s 防穿透
} else {
redis.setex(key, 3600, val);
}
return val;
}
3.3 防护二:布隆过滤器
在缓存之前加一层布隆过滤器,非法 id 直接拦截,连 Redis 都不用访问。适合 id 空间有限、可预热的场景:
// 写入时同步放入布隆过滤器
bloomFilter.put(productId);
// 查询前先判断,一定不存在的直接拦截
if (!bloomFilter.mightContain(productId)) {
return null; // 100% 不存在,杜绝穿透
}
String val = redis.get("product:" + productId);
四、三类故障对比速查
| 维度 | 缓存雪崩 | 缓存击穿 | 缓存穿透 |
|---|---|---|---|
| 触发条件 | 大量 key 同时失效 / Redis 宕机 | 单热点 key 失效瞬间 | 查询不存在的数据 |
| 影响范围 | 全局、最严重 | 单热点、局部 | 单类非法请求 |
| 典型现象 | 命中率骤降 + DB QPS 暴涨 | 单 key 查库 QPS 尖刺 | DB 持续被空查 |
| 核心防护 | 错峰过期 + 多级缓存 + 熔断 | 互斥锁 / 逻辑过期 | 空值缓存 + 布隆过滤器 |
五、生产落地检查清单
| 检查项 | 建议做法 |
|---|---|
| 过期策略 | 基础 TTL + 随机抖动,禁止批量相同 TTL |
| 热点保护 | 热点 key 永不过期 + 逻辑过期异步刷新 |
| 空值处理 | 查空也写短期空值,封掉重复穿透 |
| 非法拦截 | 接入布隆过滤器,前置拦截 |
| 降级熔断 | DB 压力大时熔断,返回兜底数据 |
| 监控告警 | 命中率 + DB QPS + 慢查询联动告警 |
将缓存压测纳入 GitHub Actions CI,在每次发版前用模拟流量验证击穿与雪崩防护是否生效;部署侧则可结合 Docker 容器化 与 Nginx 反向代理 做统一的限流与超时控制。
六、真实排障案例:一次大促前的缓存演练
某次大促前的全链路压测中,监控发现 Redis 命中率从平时的 99% 骤降到 60%,数据库 CPU 随之冲高。排查定位为「批量商品预热任务」给上万 key 写入了完全相同的 2 小时 TTL,导致整点集体失效、瞬间雪崩。修复方案是在预热写入时引入随机抖动(见 1.3 节),并把头部热点商品改为逻辑过期 + 异步刷新。改完后再次压测,命中率稳定在 98% 以上,数据库峰值 QPS 下降约 70%。这个案例说明:缓存故障往往不是「没做防护」,而是「防护在边界条件下失效」,必须在发版前用真实流量模型反复验证,而不是上线后等问题暴露。
七、总结
缓存雪崩、击穿、穿透是 Redis 高并发场景下最高频的三类故障,但成因与解法各不相同:雪崩重「错峰与兜底」,击穿重「单点互斥」,穿透重「空值与过滤」。把它们纳入上线前的缓存设计评审,配合 Redis 缓存设计规范 的标准化实践,才能在流量洪峰来临时稳如磐石。




