2、缓存穿透、击穿、雪崩的调试与排查:现象复现、日志分析、监控指标定位
2.1 三类缓存异常的故障现象与根因对比
缓存穿透、击穿、雪崩是后端缓存层最典型的三种故障模式。虽然表象都是“请求绕过缓存直达数据库”,但触发条件、影响范围、恢复策略截然不同。调试排查的第一步,是能根据监控曲线和日志特征快速区分三者。
| 故障类型 | 触发条件 | 影响范围 | 数据库压力特征 | 恢复难度 |
|---|---|---|---|---|
| 缓存穿透 | 查询一个根本不存在的数据(如恶意攻击 ID=-1) | 单个 key 或少量 key | 持续恒定高负载,无热点倾斜 | 低(布隆过滤器/空值缓存) |
| 缓存击穿 | 一个热点 key 在缓存失效的瞬间,大量并发请求同时穿透 | 单个热点 key | 瞬间尖刺,然后快速回落 | 中(互斥锁/逻辑过期) |
| 缓存雪崩 | 大量 key 同时过期,或缓存节点宕机 | 全局/大面积 key | 持续高负载,可能压垮数据库 | 高(过期时间打散/高可用集群) |
调试直觉: 观察数据库 QPS 曲线形状——平直线大概率是穿透,尖峰大概率是击穿,持续爬坡后平台大概率是雪崩。
2.2 现象复现:构造可控的测试环境
调试缓存异常不能依赖生产环境随机触发,必须能在开发/测试环境稳定复现。以下是三类故障的复现方案:
2.2.1 缓存穿透复现
- 准备一个不存在的 key,例如
user:999999。 - 使用压测工具(wrk/vegeta)以恒定 QPS 请求该 key。
- 观察数据库连接数是否持续上升,缓存命中率是否归零。
2.2.2 缓存击穿复现
- 选择一个热点 key,设置极短的过期时间(如 1 秒)。
- 启动 50 个并发协程/线程同时请求该 key。
- 观察数据库是否在 key 过期瞬间出现 QPS 尖刺。
2.2.3 缓存雪崩复现
- 批量写入 1000 个 key,所有 key 的过期时间设为相同值(如 5 秒)。
- 持续发送请求覆盖这些 key。
- 观察 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 图展示了从“收到告警”到“定位故障类型”的完整排查决策路径:
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 变化率 | 判断负载是尖刺还是持续上升 |
调试建议: 在开发环境搭建一套与生产配置一致的缓存环境(包括过期策略、连接池大小),并编写自动化测试脚本,每次上线前自动执行穿透/击穿/雪崩场景的回归验证。