缓存穿透与雪崩的定义与危害
缓存穿透与缓存雪崩是分布式缓存系统中两种最典型的高危故障模式。虽然两者都表现为大量请求绕过缓存直接击穿后端存储,但成因、攻击路径和治理手段截然不同。本章从定义、危害、典型场景三个维度建立区分框架,为后续章节的防御实现打下理论基础。
1.1 概念区分
| 维度 | 缓存穿透 | 缓存雪崩 |
|---|---|---|
| 核心定义 | 请求查询一个根本不存在的数据,缓存永远无法命中,每次请求都直接穿透到数据库 | 缓存中大量key同时过期(或因缓存节点宕机),导致大量请求同时涌入数据库,引发连锁崩溃 |
| 触发条件 | 恶意攻击或业务缺陷查询不存在的数据(如连续递增id但id= -1) | 缓存过期时间设置相同、缓存服务宕机、流量瞬时激增 |
| 数据库压力特征 | 恒定高频请求打到底层(即使每次只查一个不存在key) | 瞬时峰值请求洪峰,可能达到正常值的数十倍 |
| 缓存侧表现 | 缓存命中率突然降至接近0%,但存储层QPS正常甚至降低(因为空值也不写缓存) | 缓存命中率瞬间暴跌,同时数据库连接数飙升,响应超时 |
1.2 业务影响与典型场景
- 数据库连接池被耗尽:每笔穿透请求都会创建并等待数据库连接,当并发达到数百、数千时,连接池迅速枯竭,后续正常请求也被阻塞。
- 慢查询积累与雪崩闭环:数据库CPU/IO打满后,正常请求也逐渐变慢,响应超时推动上游重试,形成“请求 → 超时 → 重试 → 更多请求”的正反馈循环。
- 典型穿透场景:恶意用户利用接口id=0、id=-1或随机UUID持续调用商品详情API;内部测试数据被删除但代码未处理空结果。
- 典型雪崩场景:所有首页推荐位缓存同时设置过期时间为凌晨2:00,恰好此时有大量爬虫开始扫描;或者Redis主节点宕机后,slave尚未提升,所有流量直接落到MySQL。
1.3 原理对比流程
正常缓存流程:Client → 查缓存(命中)→ 直接返回。未命中时:Client → 查缓存(miss)→ 查DB → 写缓存 → 返回。
穿透流程:Client → 查缓存(miss,key不存在)→ 查DB(找不到记录)→ 不写缓存 → 下次相同key再次穿透。
雪崩流程:大量key同时过期 → 同时查DB → DB负载骤增 → 部分DB线程阻塞 → 缓存的写操作也被阻塞 → 更多key因无法写入而失效 → 雪崩。
1.4 技术实现示例:模拟穿透与雪崩
下面用伪代码演示一个无防御的简单缓存查询方法,暴露穿透和雪崩风险。后续章节将逐步加入布隆过滤器、锁、过期随机化等防御。
// 模拟缓存穿透(无空值缓存)
public Book getBook(String id) {
Book book = cache.get(id);
if (book != null) return book;
// 缓存未命中 -> 查数据库
book = db.query("SELECT * FROM books WHERE id = ?", id);
if (book != null) {
cache.put(id, book, 3600); // 仅对有值key写缓存
}
// 若book为null则什么都不写 → 下次相同id继续穿透DB
return book;
}
// 模拟缓存雪崩(所有key同时过期)
public List<Book> getHotBooks() {
List<Book> books = cache.get("hot_books");
if (books != null) return books;
// 缓存过期时,大量请求同时到达DB
books = db.query("SELECT * FROM books WHERE hot = 1 LIMIT 100");
cache.put("hot_books", books, 120); // 固定过期时间120秒
return books;
}
常见错误陷阱:只对有值结果写入缓存,而忽视空值缓存;所有热点key使用相同的固定过期时间;未对缓存节点做高可用部署。这三条是生产环境中引发穿透和雪崩的最普遍原因。
1.5 部署层面引发的危害放大
- 连接池耗尽:MySQL默认最大连接数通常为151~200,一次穿透雪崩即可打满。
- CPU耗尽与慢查询堆积:大量重复全表扫描或没有索引的查询拖垮数据库IO。
- 跨服务级联崩溃:数据库崩溃后,上游的微服务因大量超时导致线程阻塞,最终触发Hystrix熔断,影响其他不相关服务。
1.6 性能与稳定性指标
| 指标 | 正常值 | 穿透/雪崩发生时 | 修复目标 |
|---|---|---|---|
| 缓存命中率 | > 95% | < 10% 甚至 0% | 恢复到 > 95% |
| 数据库QPS | 500~2000 | 暴增至 10000+(取决于并发) | 回落至正常值 |
| 99%响应时间 | < 10ms | > 2s 或 timeout | 恢复至 < 20ms |
| 错误率 | < 0.1% | > 20% 甚至全量失败 | 降至 0% |
1.7 小结与后续章节映射
本章厘清了穿透与雪崩的本质差异:穿透是空key永久绕过缓存,雪崩是大量key同时失效导致洪峰。后续章节将逐个实现布隆过滤器(第3章)、空值缓存(第4章)、互斥锁与限流(第5章)、过期时间随机化(第6章)、缓存双副本高可用(第8章)等技术,最终构建一套完整的防御体系。
学习建议:在学习后续实现章节前,建议先在本地或云环境模拟本章描述的两种场景(例如使用JMeter并发请求一个不存在的id,或设置一批redis key的ttl相同)。亲身观察数据库连接数和响应时间的变化,会对防御手段的收益有更直观的感受。