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 排查步骤

  1. 确认命中率:通过 Redis INFO stats 或监控面板查看 keyspace_hits 与 keyspace_misses 比值。
  2. 分析 Key 分布:抓取一段时间内未命中 Key 的样本,检查是否集中在某个前缀或模式(如 user:-1、product:00000)。
  3. 检查数据库日志:确认是否存在大量空结果查询,且 SQL 语句无索引或全表扫描。
  4. 验证布隆过滤器:若已部署布隆过滤器,检查其误判率是否过高或过滤器未加载。

4.2.3 应急处理方案

  • 缓存空值:对数据库查询为空的结果,也写入缓存(TTL 设为 30~60 秒),防止重复穿透。
  • 布隆过滤器:在缓存层前置布隆过滤器,拦截不存在 Key 的请求。
  • 参数校验:在 API 网关层对请求参数做合法性校验(如 ID 必须为正整数)。
  • 限流熔断:对数据库连接池设置最大并发阈值,超出后直接返回降级响应。
最佳实践:布隆过滤器 + 空值缓存组合使用。布隆过滤器拦截大部分非法请求,空值缓存兜底处理合法但数据不存在的情况。

4.3 缓存击穿

4.3.1 现象与根因

缓存击穿特指单个热点 Key 在过期瞬间,大量并发请求同时回源查询数据库。常见于秒杀商品详情、热搜榜单等场景。

危险信号:数据库单表 QPS 在几毫秒内从 100 飙升至 10000+,且该表对应的缓存 Key 恰好在该时间点过期。

4.3.2 排查步骤

  1. 定位热点 Key:使用 Redis hotkeys 命令或监控工具(如 CacheCloud)找出访问频率最高的 Key。
  2. 检查 Key 过期时间:确认该 Key 的 TTL 是否设置过短(如 1 秒),或未设置随机偏移。
  3. 分析并发来源:查看请求日志,确认同一时间窗口内对该 Key 的请求是否来自同一用户群体或同一接口。
  4. 验证锁机制:检查代码中是否实现了互斥锁(如 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 排查步骤

  1. 检查过期时间分布:扫描 Redis 中所有 Key 的 TTL,统计过期时间是否集中在同一秒或同一分钟。
  2. 确认缓存节点状态:通过 CLUSTER INFO 或 Sentinel 日志确认是否有主节点宕机、主从切换或网络分区。
  3. 分析数据库连接池:查看数据库活跃连接数是否达到上限,以及慢查询日志中是否出现大量相同 SQL。
  4. 检查客户端重试策略:确认 Redis 客户端是否开启了自动重试,重试间隔是否过短导致雪崩加剧。

4.4.3 应急处理方案

  • 过期时间随机化:在基础 TTL 上增加随机偏移(如 ±30%),避免 Key 同时过期。
  • 缓存集群高可用:部署 Redis Sentinel 或 Cluster,确保单节点故障时自动切换。
  • 多级缓存:本地缓存(Caffeine/Guava)作为 L1,Redis 作为 L2,数据库作为 L3,逐级降级。
  • 限流与熔断:在网关层对回源请求做限流,数据库连接池设置最大等待队列长度。
快速止血:若缓存节点已宕机,可临时将缓存读取降级为直接读数据库,但必须配合限流(如每秒最多 500 次回源),防止数据库被压垮。

4.5 三种故障的排查流程对比

缓存穿透 / 击穿 / 雪崩 排查决策树 缓存命中率下降 检查 Key 是否存在 穿透:Key 不存在 → 布隆过滤器 / 空值缓存 击穿:单个热点 Key → 互斥锁 / 逻辑过期 雪崩:大量 Key 过期 → TTL 随机化 / 集群高可用 数据库压力是否持续? 是 启动限流 + 熔断 否 恢复缓存,观察

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 综合应急处理流程

  1. 第一优先级:保护数据库 — 立即在数据库连接池或网关层开启限流,拒绝超过阈值的请求。
  2. 第二优先级:恢复缓存服务 — 若为雪崩,重启缓存集群或触发主从切换;若为穿透/击穿,临时写入空值或加锁重建缓存。
  3. 第三优先级:定位根因 — 按上述排查清单逐一检查,确定故障类型。
  4. 第四优先级:永久修复 — 根据根因实施布隆过滤器、TTL 随机化、热点 Key 永不过期等长期方案。
注意:在未确定故障类型前,不要盲目重启缓存节点。若为穿透攻击,重启缓存只会让数据库承受更多压力。先限流,再诊断,后恢复。