3. 缓存击穿原理:热点Key失效瞬间的并发风暴、互斥锁与逻辑过期方案对比、击穿场景的流量特征
3.1 缓存击穿的本质:热点Key失效的并发风暴
缓存击穿是指一个被高频并发访问的热点Key在缓存中恰好过期失效的瞬间,大量请求同时穿透缓存层,直接涌向后端数据库或下游服务,导致数据库负载瞬间飙升甚至崩溃的现象。它与缓存雪崩的区别在于:雪崩是大量Key同时失效,而击穿是单个热点Key失效引发的集中穿透。
核心特征:热点Key + 高并发 + 失效瞬间 = 数据库瞬时压力峰值可达正常值的 10~100 倍。
3.2 互斥锁方案:强一致性下的并发控制
互斥锁(Mutex)方案的核心思想是:当缓存失效时,只允许一个线程去加载数据,其他线程等待该线程完成后再从缓存获取。这是最直观的击穿防护手段,适用于对数据一致性要求极高的场景。
3.2.1 实现流程
- 检查缓存:请求到达后,先查询缓存是否存在。
- 获取锁:若缓存不存在,尝试获取分布式锁(如 Redis SETNX)。
- 双重检查:获取锁成功后,再次检查缓存(防止等待期间其他线程已重建)。
- 加载数据:从数据库加载数据,写入缓存并设置过期时间。
- 释放锁:完成写入后释放锁,其他等待线程从缓存读取。
// 伪代码:互斥锁实现缓存击穿防护
public String getData(String key) {
String value = redis.get(key);
if (value != null) {
return value;
}
// 尝试获取分布式锁
String lockKey = "lock:" + key;
boolean locked = redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS);
if (locked) {
try {
// 双重检查:防止等待期间其他线程已重建
value = redis.get(key);
if (value != null) {
return value;
}
// 从数据库加载
value = db.query(key);
redis.setex(key, 300, value);
return value;
} finally {
redis.del(lockKey); // 释放锁
}
} else {
// 未获取到锁,等待后重试
Thread.sleep(50);
return getData(key); // 递归重试
}
}
3.2.2 互斥锁方案评估
| 维度 | 说明 | 评价 |
|---|---|---|
| 数据一致性 | 严格保证缓存与数据库一致,无脏数据风险 | 高 |
| 并发性能 | 锁等待期间请求阻塞,吞吐量下降 | 中 |
| 实现复杂度 | 需处理锁超时、死锁、重入等问题 | 中 |
| 适用场景 | 秒杀库存、金融账户、配置中心等强一致场景 | 推荐 |
避坑提示:互斥锁必须设置合理的超时时间,防止线程崩溃导致死锁。建议使用 Redisson 等成熟框架的看门狗机制自动续期。
3.3 逻辑过期方案:高并发下的最终一致性
逻辑过期方案不依赖物理过期时间,而是在缓存数据中内嵌一个过期时间戳。当请求发现逻辑过期时,由一个线程负责异步重建缓存,其他线程直接返回旧数据。这种方案牺牲了强一致性,但换取了极高的吞吐量。
3.3.1 数据模型与流程
// 逻辑过期数据模型
public class CacheData<T> {
private T data; // 实际数据
private long expireTime; // 逻辑过期时间戳
private boolean loading; // 是否正在重建
}
// 伪代码:逻辑过期实现
public T getData(String key) {
CacheData<T> cacheData = redis.get(key);
if (cacheData == null) {
// 缓存不存在,同步加载
return loadDataSync(key);
}
if (cacheData.expireTime > System.currentTimeMillis()) {
// 未过期,直接返回
return cacheData.data;
}
// 逻辑过期,尝试获取重建锁
String lockKey = "reload:" + key;
boolean locked = redis.setnx(lockKey, "1", 1, TimeUnit.SECONDS);
if (locked) {
// 获取锁的线程负责异步重建
executor.submit(() -> {
T newData = db.query(key);
CacheData<T> newCache = new CacheData<>(
newData, System.currentTimeMillis() + 300000
);
redis.set(key, newCache);
redis.del(lockKey);
});
}
// 无论是否获取到锁,都返回旧数据
return cacheData.data;
}
3.3.2 互斥锁 vs 逻辑过期方案对比
| 对比维度 | 互斥锁方案 | 逻辑过期方案 |
|---|---|---|
| 数据一致性 | 强一致,所有请求看到最新数据 | 最终一致,部分请求读到旧数据 |
| 并发吞吐量 | 锁等待导致吞吐量下降 | 无等待,直接返回旧数据,吞吐量高 |
| 数据库压力 | 仅一个线程查询数据库 | 仅一个线程异步查询数据库 |
| 响应延迟 | 锁等待线程延迟增加 | 所有请求即时响应 |
| 实现复杂度 | 中等,需处理锁超时 | 较高,需管理异步线程池 |
| 适用场景 | 秒杀、金融、配置 | 热搜、榜单、推荐流 |
3.4 击穿场景的流量特征与识别
在实际生产环境中,缓存击穿并非随机发生,而是具有明显的流量特征。掌握这些特征有助于提前识别和预防击穿风险。
3.4.1 流量特征清单
| 特征维度 | 正常流量 | 击穿流量 | 识别指标 |
|---|---|---|---|
| QPS 曲线 | 平滑波动 | 瞬间尖峰,呈脉冲状 | QPS 突增 5~20 倍 |
| 缓存命中率 | 稳定在 90% 以上 | 瞬间跌至 0% 或接近 0% | 命中率断崖式下降 |
| 数据库连接数 | 稳定在连接池 30%~50% | 瞬间打满连接池 | 活跃连接数 100% |
| 响应延迟 | P99 < 50ms | P99 > 500ms 甚至超时 | 延迟急剧上升 |
| 错误率 | < 0.1% | 5%~30% 超时或拒绝 | 错误率飙升 |
| 热点Key分布 | 均匀分散 | 集中在 1~3 个 Key | Key 访问频次极不均匀 |
3.4.2 典型击穿场景
- 秒杀/抢购:商品详情页的库存 Key 在秒杀开始时恰好过期,大量用户同时刷新。
- 热点新闻/热搜:突发新闻导致某个话题 Key 访问量瞬间暴增,且 Key 恰好在此时过期。
- 定时任务失效:缓存预热定时任务因故障未执行,导致业务高峰期 Key 失效。
- 缓存过期时间设置不当:所有热点 Key 的过期时间集中在同一时刻,形成人为击穿。
最佳实践:在监控系统中设置缓存命中率告警(阈值 < 80%)、数据库连接数告警(阈值 > 80%)、热点 Key 访问频次告警(单个 Key QPS > 总 QPS 的 30%),实现击穿风险的实时感知。
3.5 方案选型决策树
在实际项目中,选择互斥锁还是逻辑过期方案,需要根据业务场景的一致性要求和并发量级综合判断。以下决策树可辅助选型:
核心结论:缓存击穿防护没有银弹。互斥锁方案保证强一致但牺牲部分性能,逻辑过期方案保证高吞吐但接受短暂不一致。实际落地时,建议对热点Key进行分级治理:一级热点(如秒杀库存)使用互斥锁 + 本地缓存双重防护;二级热点(如商品详情)使用逻辑过期 + 异步刷新;普通Key使用标准过期策略 + 限流兜底。