3. 缓存击穿:定义与成因、互斥锁方案、逻辑过期方案、热点数据识别策略
3.1 定义与成因
缓存击穿(Cache Breakdown)是指某个热点 Key 在缓存中恰好过期失效,而此时有大量并发请求同时访问该 Key,导致所有请求直接穿透缓存,瞬间将压力集中到后端数据库(DB),造成数据库负载飙升甚至宕机。
与缓存雪崩(大量 Key 同时失效)不同,击穿聚焦于单个热点 Key 的失效场景。其核心成因包括:
- 热点 Key 过期:业务高峰期,某个高频访问的 Key 到达 TTL 上限,缓存中数据被清除。
- 并发穿透:在 Key 失效的瞬间,大量请求同时到达,缓存层无法命中,全部回源到数据库。
- 重建开销大:该 Key 对应的数据在数据库中查询或计算成本较高(如复杂 SQL、聚合计算),导致 DB 响应变慢,进一步加剧请求堆积。
关键指标:缓存击穿发生时,DB 的 QPS 可在毫秒级内从正常值飙升至正常值的 10~100 倍,且热点 Key 的访问量越大,击穿破坏力越强。
3.2 互斥锁方案(Mutex Lock)
互斥锁方案的核心思想是:当缓存失效时,只允许一个线程去数据库加载数据,其他线程等待该线程完成加载后,直接从缓存中获取数据。常见的实现方式包括 Redis SETNX 或 Redisson 分布式锁。
3.2.1 流程步骤
- 请求到达,查询缓存,未命中。
- 尝试获取分布式锁(如
SETNX lock_key 1 EX 10)。 - 若获取锁成功,则查询数据库,重建缓存,释放锁。
- 若获取锁失败,则等待短暂时间(如 50ms)后重试,再次查询缓存。
3.2.2 代码示例(伪代码)
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", 10, TimeUnit.SECONDS);
if (locked) {
try {
// 双重检查:防止在等待锁期间,其他线程已重建缓存
value = redis.get(key);
if (value != null) {
return value;
}
// 查询数据库
value = db.query(key);
redis.set(key, value, 300, TimeUnit.SECONDS);
return value;
} finally {
redis.del(lockKey);
}
} else {
// 未获取到锁,等待后重试
Thread.sleep(50);
return getData(key); // 递归重试
}
}
3.2.3 优缺点对比
| 维度 | 说明 |
|---|---|
| 优点 | 实现简单,数据一致性高,能有效防止缓存击穿。 |
| 缺点 | 存在锁竞争开销;若锁服务(Redis)宕机,可能引发死锁或大面积阻塞;高并发下线程等待会消耗 CPU。 |
| 适用场景 | 对数据一致性要求高、并发量可控的热点 Key 场景。 |
避坑提示:锁的超时时间需合理设置。过短可能导致缓存未重建完锁就释放,引发多个线程同时回源;过长则可能阻塞其他正常请求。建议锁超时时间 = 预估 DB 查询时间 × 2 + 网络开销。
3.3 逻辑过期方案(Logical Expiration)
逻辑过期方案不依赖 Redis 的 TTL 物理过期,而是在缓存数据中额外存储一个过期时间字段(逻辑过期时间)。当请求发现数据逻辑过期时,立即返回旧数据,同时异步启动一个线程去更新缓存。该方案的核心是用短暂的数据不一致换取系统的高可用。
3.3.1 数据结构设计
// 缓存中的数据结构
{
"data": { ... }, // 实际业务数据
"expireTime": 1700000000 // 逻辑过期时间戳(毫秒)
}
3.3.2 流程步骤
- 请求到达,查询缓存,获取到数据及逻辑过期时间。
- 判断当前时间是否大于
expireTime: - 若未过期:直接返回数据。
- 若已过期:返回旧数据,并尝试获取分布式锁(用于控制只有一个线程执行缓存更新)。
- 获取锁成功的线程,异步执行缓存重建(查询 DB、更新缓存、更新逻辑过期时间)。
- 获取锁失败的线程,直接返回旧数据,不等待。
3.3.3 优缺点对比
| 维度 | 说明 |
|---|---|
| 优点 | 请求无需等待锁释放,响应速度快;对 DB 压力小(仅单线程异步更新);适合读多写少的场景。 |
| 缺点 | 存在短暂的数据不一致(旧数据被返回);实现复杂度高于互斥锁;需要额外维护逻辑过期时间。 |
| 适用场景 | 对数据实时性要求不高、允许短暂脏读的热点数据(如首页推荐、排行榜)。 |
实践建议:逻辑过期时间通常设置为物理 TTL 的 60%~80%。例如物理 TTL 为 5 分钟,逻辑过期时间设为 3~4 分钟,确保在物理过期前有足够时间异步刷新。
3.4 热点数据识别策略
要有效防御缓存击穿,首先需要精准识别哪些 Key 属于热点数据。以下是业界常用的识别策略:
3.4.1 识别方法对比
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 访问频率统计 | 在 Redis 或应用层维护每个 Key 的访问计数器(如 LFU、LRU 近似统计)。 | 实时性高,能动态发现热点。 | 需要额外存储开销;高并发下计数器本身可能成为瓶颈。 |
| 业务规则预判 | 根据业务特征提前定义热点(如秒杀商品 ID、大 V 用户信息、热门新闻 ID)。 | 实现简单,无需实时计算。 | 无法覆盖突发热点;维护成本高。 |
| 滑动窗口采样 | 在固定时间窗口(如 1 秒、10 秒)内统计 Key 的访问次数,超过阈值则标记为热点。 | 能有效过滤瞬时尖峰,避免误判。 | 窗口大小和阈值需要调参;对短时高频访问可能漏判。 |
| Redis 内置命令 | 使用 INFO keyspace 或 MONITOR 命令分析 Key 访问模式。 |
无需额外开发,直接利用 Redis 能力。 | MONITOR 会降低 Redis 性能;INFO 命令粒度较粗。 |
3.4.2 热点数据识别流程图
3.5 方案选型建议
| 场景特征 | 推荐方案 | 理由 |
|---|---|---|
| 数据一致性要求极高(如支付、库存) | 互斥锁方案 | 保证同一时刻只有一个线程回源,数据强一致。 |
| 允许短暂不一致,追求高吞吐(如资讯、榜单) | 逻辑过期方案 | 请求无阻塞,用户体验好,DB 压力小。 |
| 热点 Key 数量少且可预判 | 业务规则预判 + 互斥锁 | 提前预热,减少锁竞争概率。 |
| 热点 Key 动态变化,无法预判 | 滑动窗口采样 + 逻辑过期 | 自动发现热点,异步更新,兼顾性能与一致性。 |
核心原则:缓存击穿的防御不是“消灭回源”,而是“控制回源的并发度”。互斥锁和逻辑过期方案本质都是将 N 次回源降为 1 次,区别在于是否让请求等待。根据业务对一致性和响应时间的容忍度,选择最合适的策略。