2、缓存穿透、击穿、雪崩的调试与排查:现象复现、日志分析、监控指标定位

2.1 三类缓存异常的故障现象与根因对比

缓存穿透、击穿、雪崩是后端缓存层最典型的三种故障模式。虽然表象都是“请求绕过缓存直达数据库”,但触发条件、影响范围、恢复策略截然不同。调试排查的第一步,是能根据监控曲线和日志特征快速区分三者。

故障类型 触发条件 影响范围 数据库压力特征 恢复难度
缓存穿透 查询一个根本不存在的数据(如恶意攻击 ID=-1) 单个 key 或少量 key 持续恒定高负载,无热点倾斜 低(布隆过滤器/空值缓存)
缓存击穿 一个热点 key 在缓存失效的瞬间,大量并发请求同时穿透 单个热点 key 瞬间尖刺,然后快速回落 中(互斥锁/逻辑过期)
缓存雪崩 大量 key 同时过期,或缓存节点宕机 全局/大面积 key 持续高负载,可能压垮数据库 高(过期时间打散/高可用集群)
调试直觉: 观察数据库 QPS 曲线形状——平直线大概率是穿透,尖峰大概率是击穿,持续爬坡后平台大概率是雪崩。

2.2 现象复现:构造可控的测试环境

调试缓存异常不能依赖生产环境随机触发,必须能在开发/测试环境稳定复现。以下是三类故障的复现方案:

2.2.1 缓存穿透复现

  1. 准备一个不存在的 key,例如 user:999999。
  2. 使用压测工具(wrk/vegeta)以恒定 QPS 请求该 key。
  3. 观察数据库连接数是否持续上升,缓存命中率是否归零。

2.2.2 缓存击穿复现

  1. 选择一个热点 key,设置极短的过期时间(如 1 秒)。
  2. 启动 50 个并发协程/线程同时请求该 key。
  3. 观察数据库是否在 key 过期瞬间出现 QPS 尖刺。

2.2.3 缓存雪崩复现

  1. 批量写入 1000 个 key,所有 key 的过期时间设为相同值(如 5 秒)。
  2. 持续发送请求覆盖这些 key。
  3. 观察 5 秒后数据库负载是否瞬间飙升。
复现检查清单:
  • 缓存服务(Redis/Memcached)是否独立部署,避免本地缓存干扰。
  • 数据库连接池是否足够大,防止复现时被限流。
  • 监控埋点是否覆盖:缓存命中率、DB QPS、线程池活跃数。

2.3 日志分析:从请求链路中定位异常

生产环境排查时,日志是还原故障现场的第一手资料。需要关注三类关键日志:

2.3.1 访问日志(Nginx/Gateway)

# 缓存穿透特征:大量请求同一个不存在的资源
192.168.1.1 - - [10/Oct/2024:10:15:30 +0800] "GET /api/user/999999 HTTP/1.1" 200 0.012
192.168.1.2 - - [10/Oct/2024:10:15:31 +0800] "GET /api/user/999999 HTTP/1.1" 200 0.011
# 缓存雪崩特征:大量不同资源的请求同时变慢
192.168.1.1 - - [10/Oct/2024:10:16:00 +0800] "GET /api/product/1001 HTTP/1.1" 200 2.345
192.168.1.2 - - [10/Oct/2024:10:16:00 +0800] "GET /api/product/1002 HTTP/1.1" 200 2.456

2.3.2 应用日志(业务服务)

# 击穿时:同一 key 的缓存加载日志密集出现
2024-10-10 10:15:30.123 [http-nio-8080-exec-8] WARN  c.e.c.CacheService - Cache miss for key=hot_product_1001, loading from DB
2024-10-10 10:15:30.124 [http-nio-8080-exec-9] WARN  c.e.c.CacheService - Cache miss for key=hot_product_1001, loading from DB
2024-10-10 10:15:30.125 [http-nio-8080-exec-10] WARN c.e.c.CacheService - Cache miss for key=hot_product_1001, loading from DB

2.3.3 慢查询日志(数据库)

# 穿透时:大量无意义的全表扫描
# Time: 2024-10-10T10:15:30.123456Z
# Query_time: 0.523  Lock_time: 0.000
SELECT * FROM users WHERE id = 999999;

# 雪崩时:大量不同表的查询同时变慢
# Time: 2024-10-10T10:16:00.000123Z
# Query_time: 2.345  Lock_time: 0.001
SELECT * FROM products WHERE id = 1001;
关键日志模式: 如果在 1 秒内看到超过 10 条“Cache miss for key=xxx”且 key 相同,基本可以判定为缓存击穿;如果 key 各不相同且都是不存在的 ID,则为缓存穿透。

2.4 监控指标定位:用数据验证故障类型

日志提供的是定性证据,监控指标提供定量证据。以下是一组核心指标及其在三种故障下的表现:

监控指标 缓存穿透 缓存击穿 缓存雪崩
缓存命中率 持续低于 10% 瞬间跌至 0%,然后快速恢复 整体从 95% 跌至 30% 以下
DB QPS 恒定高负载(如 5000/s) 瞬间尖刺(如 10000/s 持续 2s) 持续上升至数据库极限
DB 响应时间 P99 稳定在 50ms 左右 尖刺到 500ms 后回落 持续恶化,从 10ms 升至 2s
Redis OPS 正常或略低 正常 可能下降(节点宕机)
线程池活跃数 持续高位 短暂打满 持续打满并触发拒绝策略

2.5 缓存异常排查流程图

以下 SVG 图展示了从“收到告警”到“定位故障类型”的完整排查决策路径:

缓存异常排查决策流程图 收到缓存告警 步骤1:检查缓存命中率曲线 命中率是否瞬间跌至 0%? 是 否 缓存击穿 检查热点 key 列表 检查 DB QPS 曲线 是否持续高负载? 处理:互斥锁 / 逻辑过期 请求 key 是否大量不存在? (检查访问日志) 是 缓存穿透 处理:布隆过滤器 / 空值缓存 否 缓存雪崩 处理:过期时间打散 / 集群 💡 核心原则:先看命中率曲线形状, 再看 DB QPS 是否持续,最后查日志确认。

2.6 实战排查案例:一次完整的雪崩定位

以下是一个真实生产案例的排查过程,展示如何综合运用日志、监控和复现手段:

时间线 现象 排查动作 发现
10:00:00 告警:DB 连接数超过 80% 查看缓存命中率面板 命中率从 97% 跌至 45%
10:00:30 DB QPS 从 2000 升至 12000 查看 Redis 节点状态 所有节点正常,无宕机
10:01:00 大量请求超时 查看应用日志,搜索 "Cache miss" 发现大量不同 key 同时 miss
10:01:30 — 检查 key 的过期时间设置 所有商品缓存统一设置为 3600 秒,整点过期
10:02:00 — 结论 缓存雪崩:大量 key 在 10:00 同时过期
根因: 业务方在凌晨上线时,将所有商品缓存设置为 expire = 3600,导致上午 10:00 整点集中过期。修复方案:过期时间增加随机偏移 expire = 3600 + random(0, 600),并启用本地二级缓存作为兜底。

2.7 调试工具与命令速查

以下是在排查过程中最常用的调试命令和工具:

工具/命令 用途 示例
redis-cli --bigkeys 找出 Redis 中占用空间最大的 key 排查雪崩时是否因大 key 导致阻塞
redis-cli monitor 实时查看 Redis 命令执行情况 观察是否有大量 GET 请求命中同一个 key
curl -w "@curl-format.txt" 测量 HTTP 请求各阶段耗时 区分是网络延迟还是服务端处理慢
jstack <pid> 打印 Java 线程堆栈 查看线程是否阻塞在数据库查询上
PromQL: rate(db_queries_total[1m]) 计算数据库 QPS 变化率 判断负载是尖刺还是持续上升
调试建议: 在开发环境搭建一套与生产配置一致的缓存环境(包括过期策略、连接池大小),并编写自动化测试脚本,每次上线前自动执行穿透/击穿/雪崩场景的回归验证。