3. 缓存击穿:定义、与穿透的区别、热点key失效场景
3.1 缓存击穿的定义
缓存击穿(Cache Breakdown / Hotspot Invalid)是指一个被频繁访问的热点 Key 在缓存中失效的瞬间,大量并发请求同时穿透到后端数据库,导致数据库负载瞬间飙升甚至崩溃的现象。
与缓存穿透不同,击穿的目标 Key 在数据库中真实存在,只是因为缓存过期或主动删除,导致在极短的时间窗口内,所有请求都直接打到数据库上。
核心特征:热点 Key + 瞬时失效 + 高并发穿透 = 数据库压力尖峰。
3.2 缓存击穿 vs 缓存穿透
| 对比维度 | 缓存击穿 | 缓存穿透 |
|---|---|---|
| 目标 Key 是否存在 | 数据库中存在(热点数据) | 数据库中不存在(非法/空数据) |
| 失效原因 | 缓存过期 / 主动淘汰 | 查询根本不存在的数据 |
| 并发影响 | 大量请求同时穿透到 DB | 每次请求都穿透到 DB |
| 数据库压力 | 瞬时尖峰(突发性) | 持续低效(持续性) |
| 典型场景 | 秒杀商品详情、热搜榜单 | 恶意攻击、参数校验缺失 |
| 解决方案 | 互斥锁、逻辑过期、预热 | 布隆过滤器、空值缓存 |
一句话区分:击穿是“有数据但缓存没了”,穿透是“本来就没数据”。
3.3 热点 Key 失效的典型场景
热点 Key 失效是缓存击穿的直接诱因,以下三类场景最容易触发:
- 定时过期:所有热点 Key 设置了相同的过期时间(如凌晨 00:00 统一过期),导致瞬间集体失效。
- 缓存淘汰:内存不足时,Redis 按 LRU/LFU 策略淘汰了热点 Key,而淘汰后立即迎来大量请求。
- 主动删除:运维或业务方手动删除热点 Key(如更新数据后清缓存),但新缓存尚未写入。
避坑提醒:不要给热点 Key 设置完全相同的过期时间。建议在基础 TTL 上增加随机偏移(如 ±10%),避免“雪崩式”集体失效。
3.4 缓存击穿流程示意图
3.5 解决方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 互斥锁(Mutex) | 第一个请求加锁查 DB,其余等待 | 实现简单,保证只有一个请求查 DB | 可能阻塞,存在死锁风险 |
| 逻辑过期 | 缓存永不过期,但带逻辑过期时间 | 无锁,性能高 | 数据短暂不一致,需异步更新 |
| 缓存预热 | 提前加载热点数据到缓存 | 避免失效窗口 | 需要预判热点,静态数据适用 |
| 二级缓存 | 本地缓存 + Redis 双层保护 | 抗并发能力强 | 本地缓存一致性维护复杂 |
生产建议:对于秒杀、热搜等极高并发场景,推荐“逻辑过期 + 异步更新”组合方案,既能保证性能,又能避免数据库被打穿。
3.6 互斥锁实现示例(伪代码)
// 伪代码:Redis 互斥锁防止缓存击穿
public String getData(String key) {
// 1. 尝试从缓存获取
String value = redis.get(key);
if (value != null) {
return value;
}
// 2. 缓存未命中,尝试获取分布式锁
String lockKey = "lock:" + key;
boolean locked = redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS);
if (locked) {
try {
// 3. 双重检查:可能其他线程已重建缓存
value = redis.get(key);
if (value != null) {
return value;
}
// 4. 查询数据库
value = db.query(key);
// 5. 回写缓存(设置随机过期时间)
int ttl = 300 + new Random().nextInt(60);
redis.setex(key, ttl, value);
return value;
} finally {
// 6. 释放锁
redis.del(lockKey);
}
} else {
// 7. 未获取到锁,等待后重试
Thread.sleep(50);
return getData(key); // 递归重试
}
}
注意:递归重试可能导致栈溢出,生产环境建议使用循环重试 + 最大等待次数限制。