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 流程步骤

  1. 请求到达,查询缓存,未命中。
  2. 尝试获取分布式锁(如 SETNX lock_key 1 EX 10)。
  3. 若获取锁成功,则查询数据库,重建缓存,释放锁。
  4. 若获取锁失败,则等待短暂时间(如 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 流程步骤

  1. 请求到达,查询缓存,获取到数据及逻辑过期时间。
  2. 判断当前时间是否大于 expireTime:
    • 若未过期:直接返回数据。
    • 若已过期:返回旧数据,并尝试获取分布式锁(用于控制只有一个线程执行缓存更新)。
  3. 获取锁成功的线程,异步执行缓存重建(查询 DB、更新缓存、更新逻辑过期时间)。
  4. 获取锁失败的线程,直接返回旧数据,不等待。

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 热点数据识别流程图

热点数据识别流程 请求流量入口 访问频率统计 业务规则预判 滑动窗口采样 阈值判定 标记为热点 Key 触发保护策略

3.5 方案选型建议

场景特征 推荐方案 理由
数据一致性要求极高(如支付、库存) 互斥锁方案 保证同一时刻只有一个线程回源,数据强一致。
允许短暂不一致,追求高吞吐(如资讯、榜单) 逻辑过期方案 请求无阻塞,用户体验好,DB 压力小。
热点 Key 数量少且可预判 业务规则预判 + 互斥锁 提前预热,减少锁竞争概率。
热点 Key 动态变化,无法预判 滑动窗口采样 + 逻辑过期 自动发现热点,异步更新,兼顾性能与一致性。
核心原则:缓存击穿的防御不是“消灭回源”,而是“控制回源的并发度”。互斥锁和逻辑过期方案本质都是将 N 次回源降为 1 次,区别在于是否让请求等待。根据业务对一致性和响应时间的容忍度,选择最合适的策略。