2. 核心概念:缓存穿透、缓存击穿、缓存雪崩的定义与区别
在构建高并发 OpenAPI 接口时,缓存层是保护数据库的第一道防线。然而,缓存层本身也存在三类经典故障场景——缓存穿透、缓存击穿和缓存雪崩。这三者虽然名称相似,但成因、影响范围和解决方案截然不同。理解它们的本质区别,是设计可靠缓存架构的前提。
2.1 缓存穿透
定义:查询一个根本不存在的数据。由于缓存中不存在该数据(因为数据本身就不存在),每次请求都会穿透缓存,直接打到数据库。如果攻击者利用大量不存在的 key 发起请求,数据库将承受巨大压力,甚至宕机。
核心特征:缓存和数据库中都没有该数据,请求直接穿透缓存层。
典型场景
- 恶意攻击:遍历不存在的用户 ID 或商品 ID。
- 程序 bug:查询条件错误,导致 key 格式异常。
- 数据删除后未同步:数据库记录已删除,但缓存未及时清理。
解决方案
- 缓存空对象:即使数据库返回空结果,也缓存一个短生命周期的空值(如
null或特殊标记),避免重复穿透。 - 布隆过滤器:在缓存层之前加一层布隆过滤器,快速判断 key 是否可能存在。如果过滤器判定不存在,直接拒绝请求。
- 参数校验:在 API 入口层对请求参数进行合法性校验,拦截明显无效的查询。
2.2 缓存击穿
定义:一个热点 key 在缓存中过期失效的瞬间,大量并发请求同时涌入,全部穿透到数据库。由于是热点数据,数据库瞬间承受高并发查询,可能导致服务雪崩。
核心特征:缓存中某个热点 key 失效,大量请求同时打到数据库。
典型场景
- 秒杀商品详情页:某个爆款商品的缓存刚好过期,大量用户同时刷新。
- 热门新闻/公告:缓存过期瞬间,所有用户请求同时穿透。
- 定时任务失效:缓存刷新周期与高并发窗口重合。
解决方案
- 互斥锁:当缓存失效时,只允许一个线程去数据库加载数据,其他线程等待或返回旧缓存。
- 逻辑过期:缓存中设置逻辑过期时间,后台异步刷新,避免热点 key 同时失效。
- 永不过期 + 主动更新:热点 key 不设置物理过期时间,由后台任务定期更新。
2.3 缓存雪崩
定义:缓存层中大量 key 同时过期,或者缓存节点宕机,导致大量请求同时穿透到数据库。与击穿不同,雪崩影响的是全局,而非单个热点 key。
核心特征:缓存层大面积失效,数据库被海量请求淹没。
典型场景
- 缓存服务重启:所有缓存数据清空,瞬间流量全部打到数据库。
- 批量设置相同过期时间:大量 key 在同一时刻过期。
- 缓存节点宕机:Redis 集群中某个分片不可用,导致该分片上的所有 key 失效。
解决方案
- 过期时间随机化:在基础过期时间上增加随机偏移,避免大量 key 同时过期。
- 缓存高可用:使用 Redis 哨兵或集群模式,避免单点故障。
- 多级缓存:本地缓存(如 Caffeine)+ 分布式缓存(如 Redis)组合,降低单层失效风险。
- 限流降级:在数据库层前加限流组件,保护数据库不被突发流量冲垮。
2.4 三者对比总结
| 维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 根本原因 | 查询不存在的数据 | 热点 key 过期 | 大量 key 同时过期或缓存宕机 |
| 影响范围 | 单个 key 或少量 key | 单个热点 key | 全局或大面积 key |
| 数据库压力 | 持续低频或高频穿透 | 瞬间高并发 | 海量并发 |
| 典型攻击 | 恶意遍历不存在的 ID | 热点数据失效瞬间 | 缓存服务重启或批量过期 |
| 核心解决方案 | 空值缓存 / 布隆过滤器 | 互斥锁 / 逻辑过期 | 过期随机化 / 多级缓存 / 限流 |
架构建议:在实际 OpenAPI 接口设计中,这三种场景往往同时存在。建议在缓存层统一实现空值缓存、热点 key 互斥锁、过期时间随机化三项基础防护,再根据业务特点叠加布隆过滤器或限流组件。
2.5 代码示例:空值缓存与互斥锁
以下是一个简化的 Java 伪代码示例,展示如何同时处理缓存穿透和缓存击穿:
public String getData(String key) {
// 1. 查缓存
String value = redis.get(key);
if (value != null) {
return value;
}
// 2. 缓存穿透防护:空值标记
if (redis.exists("null_flag:" + key)) {
return null; // 直接返回空,避免穿透
}
// 3. 缓存击穿防护:互斥锁
String lockKey = "lock:" + key;
boolean locked = redis.setnx(lockKey, "1", 3); // 3秒超时
if (!locked) {
Thread.sleep(50);
return redis.get(key); // 重试或返回旧值
}
try {
// 4. 查数据库
value = database.query(key);
if (value == null) {
// 缓存空对象,过期时间较短
redis.set("null_flag:" + key, "1", 60);
} else {
redis.set(key, value, 300);
}
return value;
} finally {
redis.del(lockKey);
}
}
注意:互斥锁方案会增加缓存层的复杂度,且存在死锁风险。生产环境中建议使用 Redisson 等成熟分布式锁框架,并设置合理的锁超时时间。