4. 缓存穿透、击穿、雪崩的原理与排查
缓存穿透、击穿与雪崩是分布式缓存系统中三种最经典的故障模式。三者表象相似(大量请求回源、数据库压力飙升),但根因与处置策略截然不同。本节从现象、根因、排查步骤与应急方案四个维度逐一拆解,并提供可复用的诊断清单。
4.1 三种故障的对比总览
| 故障类型 | 核心现象 | 根因 | 影响范围 | 典型恢复时间 |
|---|---|---|---|---|
| 缓存穿透 | 查询不存在的数据,缓存与数据库均无记录 | 恶意攻击或业务漏洞,大量请求命中空值 | 单 Key 或少量 Key | 秒级(布隆过滤器) |
| 缓存击穿 | 热点 Key 过期瞬间,大量并发请求直击数据库 | 热点 Key 过期 + 高并发 | 单个热点 Key | 毫秒~秒级(互斥锁) |
| 缓存雪崩 | 大量 Key 同时过期或缓存节点宕机,数据库被压垮 | 批量 Key 过期时间相同 / 缓存集群故障 | 全局或大面积 | 分钟级(集群恢复) |
4.2 缓存穿透
4.2.1 现象与根因
缓存穿透指请求的数据在缓存和数据库中都不存在,每次请求都绕过缓存直达数据库。攻击者常利用此漏洞构造大量不存在的 Key(如 user_id=-1),导致数据库连接耗尽。
关键指标:缓存命中率骤降(如从 95% 跌至 30%),数据库 QPS 飙升,但缓存内存使用率无明显变化。
4.2.2 排查步骤
- 确认命中率:通过 Redis
INFO stats或监控面板查看keyspace_hits与keyspace_misses比值。 - 分析 Key 分布:抓取一段时间内未命中 Key 的样本,检查是否集中在某个前缀或模式(如
user:-1、product:00000)。 - 检查数据库日志:确认是否存在大量空结果查询,且 SQL 语句无索引或全表扫描。
- 验证布隆过滤器:若已部署布隆过滤器,检查其误判率是否过高或过滤器未加载。
4.2.3 应急处理方案
- 缓存空值:对数据库查询为空的结果,也写入缓存(TTL 设为 30~60 秒),防止重复穿透。
- 布隆过滤器:在缓存层前置布隆过滤器,拦截不存在 Key 的请求。
- 参数校验:在 API 网关层对请求参数做合法性校验(如 ID 必须为正整数)。
- 限流熔断:对数据库连接池设置最大并发阈值,超出后直接返回降级响应。
最佳实践:布隆过滤器 + 空值缓存组合使用。布隆过滤器拦截大部分非法请求,空值缓存兜底处理合法但数据不存在的情况。
4.3 缓存击穿
4.3.1 现象与根因
缓存击穿特指单个热点 Key 在过期瞬间,大量并发请求同时回源查询数据库。常见于秒杀商品详情、热搜榜单等场景。
危险信号:数据库单表 QPS 在几毫秒内从 100 飙升至 10000+,且该表对应的缓存 Key 恰好在该时间点过期。
4.3.2 排查步骤
- 定位热点 Key:使用 Redis
hotkeys命令或监控工具(如 CacheCloud)找出访问频率最高的 Key。 - 检查 Key 过期时间:确认该 Key 的 TTL 是否设置过短(如 1 秒),或未设置随机偏移。
- 分析并发来源:查看请求日志,确认同一时间窗口内对该 Key 的请求是否来自同一用户群体或同一接口。
- 验证锁机制:检查代码中是否实现了互斥锁(如 SETNX),以及锁的粒度是否覆盖了回源查询。
4.3.3 应急处理方案
- 互斥锁(Mutex):在回源查询数据库前加分布式锁,只允许一个线程重建缓存,其他线程等待或直接返回旧缓存。
- 逻辑过期:缓存中存储数据的同时存储过期时间,由后台线程异步刷新,不依赖 Redis 的 TTL 过期。
- 永不过期 + 主动更新:热点 Key 不设置 TTL,通过定时任务或消息队列异步更新缓存内容。
// 互斥锁示例(Java + Redis)
public String getProduct(String key) {
String value = redis.get(key);
if (value != null) {
return value;
}
// 尝试获取锁,超时时间 3 秒
if (redis.setnx("lock:" + key, "1", 3)) {
try {
value = db.query(key);
redis.set(key, value, 60);
} finally {
redis.del("lock:" + key);
}
} else {
// 等待 50ms 后重试
Thread.sleep(50);
return getProduct(key);
}
return value;
}
4.4 缓存雪崩
4.4.1 现象与根因
缓存雪崩指大量 Key 同时过期或缓存节点整体宕机,导致所有请求瞬间压向数据库。与击穿不同,雪崩影响的是全局缓存能力。
典型场景:凌晨 00:00 所有缓存 Key 统一过期,数据库连接池瞬间被打满,应用响应超时率超过 80%。
4.4.2 排查步骤
- 检查过期时间分布:扫描 Redis 中所有 Key 的 TTL,统计过期时间是否集中在同一秒或同一分钟。
- 确认缓存节点状态:通过
CLUSTER INFO或 Sentinel 日志确认是否有主节点宕机、主从切换或网络分区。 - 分析数据库连接池:查看数据库活跃连接数是否达到上限,以及慢查询日志中是否出现大量相同 SQL。
- 检查客户端重试策略:确认 Redis 客户端是否开启了自动重试,重试间隔是否过短导致雪崩加剧。
4.4.3 应急处理方案
- 过期时间随机化:在基础 TTL 上增加随机偏移(如 ±30%),避免 Key 同时过期。
- 缓存集群高可用:部署 Redis Sentinel 或 Cluster,确保单节点故障时自动切换。
- 多级缓存:本地缓存(Caffeine/Guava)作为 L1,Redis 作为 L2,数据库作为 L3,逐级降级。
- 限流与熔断:在网关层对回源请求做限流,数据库连接池设置最大等待队列长度。
快速止血:若缓存节点已宕机,可临时将缓存读取降级为直接读数据库,但必须配合限流(如每秒最多 500 次回源),防止数据库被压垮。
4.5 三种故障的排查流程对比
4.6 故障排查清单
| 排查步骤 | 穿透 | 击穿 | 雪崩 | 工具/命令 |
|---|---|---|---|---|
| 1. 确认缓存命中率 | ✔ 必做 | ✔ 必做 | ✔ 必做 | INFO stats / Grafana |
| 2. 分析 Key 分布 | ✔ 必做 | ✔ 必做 | ✔ 必做 | --bigkeys / RDB 分析 |
| 3. 检查 TTL 设置 | 可选 | ✔ 必做 | ✔ 必做 | TTL key / 批量扫描 |
| 4. 检查缓存节点状态 | 可选 | 可选 | ✔ 必做 | CLUSTER INFO / Sentinel |
| 5. 检查数据库连接池 | ✔ 必做 | ✔ 必做 | ✔ 必做 | Druid / HikariCP 监控 |
| 6. 验证锁/限流机制 | 可选 | ✔ 必做 | ✔ 必做 | Redis SETNX / Sentinel |
4.7 综合应急处理流程
- 第一优先级:保护数据库 — 立即在数据库连接池或网关层开启限流,拒绝超过阈值的请求。
- 第二优先级:恢复缓存服务 — 若为雪崩,重启缓存集群或触发主从切换;若为穿透/击穿,临时写入空值或加锁重建缓存。
- 第三优先级:定位根因 — 按上述排查清单逐一检查,确定故障类型。
- 第四优先级:永久修复 — 根据根因实施布隆过滤器、TTL 随机化、热点 Key 永不过期等长期方案。
注意:在未确定故障类型前,不要盲目重启缓存节点。若为穿透攻击,重启缓存只会让数据库承受更多压力。先限流,再诊断,后恢复。