3. 缓存击穿与互斥锁:热点key击穿场景、分布式互斥锁实现、SETNX与Redisson方案、锁超时与续期机制

3.1 热点Key击穿场景与问题分析

缓存击穿(Cache Breakdown)特指单个热点Key在缓存中过期失效的瞬间,大量并发请求同时穿透缓存直达数据库,导致数据库负载瞬间飙升甚至宕机。与缓存雪崩(大量Key同时失效)不同,击穿聚焦于单一热点资源的失效瞬间。

典型击穿特征:
  • 单点热点:如秒杀商品库存、微博热搜、大V粉丝列表。
  • 瞬时高并发:失效瞬间QPS可达数万至数十万。
  • 数据库压力尖峰:穿透请求直接打到DB,引发连接池耗尽、慢查询堆积。

下图展示了热点Key失效瞬间的并发穿透路径:

热点Key击穿:并发穿透流程 客户端集群 缓存层 (Key已过期) 数据库 (负载飙升) 击穿三要素 1. 热点Key在缓存中失效(TTL到期或被逐出) 2. 大量请求同时到达,缓存Miss 3. 所有请求直接查询数据库,无任何保护 后果:数据库QPS瞬间放大N倍(N=并发请求数),引发雪崩式故障

3.2 分布式互斥锁实现原理

解决缓存击穿的核心思路是互斥锁(Mutex Lock):只让一个请求去数据库加载数据,其他请求等待或快速失败。在分布式环境下,必须使用分布式互斥锁来协调多个进程/实例。

维度 本地锁(synchronized/ReentrantLock) 分布式锁(Redis/Redisson)
作用范围 单进程内 跨进程、跨实例
锁状态存储 JVM内存 Redis/ZooKeeper等外部存储
适用场景 单体应用 微服务、多实例部署
容错性 进程崩溃锁自动释放 需额外机制(超时/续期)
设计原则:分布式锁在缓存击穿场景中只用于回源加载的临界区保护,不应包裹整个业务逻辑。锁粒度越小,系统吞吐越高。

3.3 SETNX方案:基础实现与缺陷

Redis的SETNX(SET if Not eXists)是最基础的分布式锁实现方式。其核心命令为:

// 加锁:key不存在时设置成功,返回1;已存在则返回0
SETNX lock_key unique_value

// 设置过期时间(防止死锁)
EXPIRE lock_key 30

// 解锁:校验唯一值后删除
if redis.get("lock_key") == unique_value:
    redis.del("lock_key")

基础SETNX方案存在以下关键缺陷:

缺陷 问题描述 后果
非原子操作 SETNX与EXPIRE分两步执行,SETNX成功后进程崩溃,EXPIRE未执行 锁永久存在,形成死锁
锁超时不可控 业务执行时间超过锁过期时间,锁自动释放 其他线程获取锁,导致并发冲突
误删锁 线程A锁超时释放,线程B加锁成功,线程A执行完误删线程B的锁 锁失效,数据不一致
不可重入 同一线程在持有锁时再次加锁会失败 递归或嵌套调用死锁
生产环境警告:不要直接使用SETNX+EXPIRE组合。Redis 2.6.12+提供了原子化的SET key value NX EX seconds命令,可解决原子性问题,但超时续期、可重入、误删等问题仍需自行处理。

3.4 Redisson方案:成熟的企业级实现

Redisson是Redis官方推荐的Java分布式锁框架,提供了完整的互斥锁、读写锁、信号量等实现。其核心优势在于自动续期(Watchdog)和可重入机制。

// Redisson分布式锁使用示例
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock("hotKey:lock");
try {
    // 尝试加锁,最多等待10秒,锁自动过期30秒
    if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
        // 回源加载数据到缓存
        String value = loadFromDB("hotKey");
        cache.set("hotKey", value, 60);
    } else {
        // 获取锁失败,从缓存获取旧数据或快速失败
        return cache.get("hotKey");
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

Redisson与基础SETNX方案的核心差异:

特性 基础SETNX Redisson
原子性 需自行保证 内置原子操作
锁超时续期 不支持 Watchdog自动续期
可重入 不支持 支持(基于计数器)
锁误删防护 需自行校验唯一值 内置唯一标识校验
等待机制 轮询+自旋 发布订阅+信号量
集群兼容性 主从切换锁丢失 RedLock算法(可选)

3.5 锁超时与续期机制(Watchdog)

锁超时是分布式锁最棘手的工程问题:超时时间设置过短,业务未完成锁已释放;设置过长,锁持有者崩溃后其他线程长时间等待。Redisson的Watchdog机制优雅地解决了这一矛盾。

Watchdog工作原理:
  1. 加锁时设置默认过期时间(30秒),不要求业务预估执行时长。
  2. 后台启动一个定时任务(每10秒执行一次),检查锁是否仍被当前线程持有。
  3. 若锁仍持有,则将锁的过期时间重置为30秒(续期)。
  4. 若线程崩溃或锁已释放,Watchdog自动停止续期,锁在30秒后自然过期。

下图展示了Watchdog的续期流程与锁状态转换:

Redisson Watchdog 锁续期机制 时间 0s 10s 20s 30s 40s 线程持有锁(业务执行中) 续 续 锁释放 Watchdog 续期规则 • 默认锁过期时间:30秒 • Watchdog检查间隔:每10秒(锁过期时间的1/3) • 续期操作:重置锁TTL为30秒 • 停止条件:锁被释放或持有线程崩溃 • 最大续期次数:无限制(业务完成即释放)

3.6 互斥锁方案对比与选型建议

方案 复杂度 可靠性 性能 推荐场景
SET NX EX(原子命令) 低 中 高 简单场景、锁持有时间可预估
SETNX + Lua脚本 中 中高 高 需自定义逻辑、避免网络开销
Redisson 低(封装完善) 高 中高 生产环境首选、复杂业务
RedLock(多节点) 高 极高 中 金融级、强一致性要求
最佳实践:
  • 优先使用Redisson,其Watchdog机制和可重入特性大幅降低工程风险。
  • 锁的粒度控制在回源加载阶段,不要包裹整个请求生命周期。
  • 设置合理的等待超时(如100ms),避免大量请求阻塞在锁等待上。
  • 结合本地缓存(如Caffeine)做二级防护,减少对分布式锁的依赖。

3.7 缓存击穿互斥锁的完整流程

综合上述技术,一个健壮的缓存击穿防护流程应包含以下步骤:

  1. 读缓存:尝试从Redis获取热点Key,命中则直接返回。
  2. 本地标记:检查本地内存标记(如Caffeine),防止同一实例内重复回源。
  3. 分布式锁:尝试获取Redisson锁(key=热点Key+锁后缀)。
  4. 双重检查:获取锁后再次检查缓存(防止等待期间其他线程已加载)。
  5. 回源加载:查询数据库,将结果写入缓存,设置合理TTL。
  6. 释放锁:业务完成后释放分布式锁,Watchdog自动停止续期。
  7. 返回数据:将数据返回给调用方。
避坑提醒:
  • 锁的Key必须与缓存Key关联但不同名,避免锁被误删。
  • 锁的唯一标识(UUID+线程ID)必须全局唯一,防止跨实例误释放。
  • Watchdog续期不是无限期的——如果业务逻辑出现死循环或长时间阻塞,锁会一直持有,需配合全局超时熔断。