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 缓存击穿流程示意图

缓存击穿流程示意图 缓存击穿流程示意图 大量并发请求 缓存是否存在? 直接返回缓存 所有请求穿透到 DB 数据库负载飙升 / 崩溃 触发条件 热点 Key 刚好过期 高并发同时到达 缓存中无数据

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); // 递归重试
    }
}
注意:递归重试可能导致栈溢出,生产环境建议使用循环重试 + 最大等待次数限制。