缓存击穿需用分布式锁而非synchronized,Redis SETNX+EX+校验value+退避重试是手写底线;Redisson因看门狗续期、阻塞等待、可重入更可靠;逻辑过期靠应用层判断expireTime并容忍脏数据。
缓存击穿发生时,
返回 null 后直接查 DB 是最危险的路径
缓存击穿本质是:一个高并发访问的热点
刚好过期,大量请求同时发现缓存未命中,全部涌向数据库。此时如果每个请求都独立执行
,DB 瞬间被打满,连接池耗尽、慢 SQL 堆积、服务雪崩。
很多人第一反应是加
,比如用
包住 DB 查询逻辑。但这只在单 JVM 进程内有效——现代微服务基本都是多实例部署,
对其他机器上的线程完全无效,锁形同虚设。
真正要拦住的是「跨进程、跨机器」的并发请求,必须靠分布式协调机制。Redis 的
(或更安全的
)天然适合做这层拦截,因为它由 Redis 单点原子性保障,所有客户端都服从同一把锁。
+
手动实现分布式锁的三个硬约束
不用框架也能快速落地,但必须守住三条底线,否则等于没锁:
必须带
(不存在才设置)和
(自动过期),缺一不可。只写
不设超时,一旦业务异常没释放锁,就永久死锁
删除锁前必须校验 value 是否匹配(防止 A 加锁、B 超时释放了 A 的锁),纯
是严重误删
获取锁失败后不能无限自旋,必须
或指数退避,否则千个线程空转 CPU,服务先于 DB 崩溃
示例关键逻辑:
为什么
的
比手写更可靠
手写锁容易漏掉几个关键细节:
Redis 8.2.3
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
下载
锁续期问题:
耗时超过 10 秒,而锁已过期,其他线程趁虚而入 ——
的看门狗(watchdog)会自动给未释放锁续命
阻塞等待逻辑:手写需自己循环
+
,而
直接封装了「最多等 3 秒,锁最长持有 10 秒」的语义
可重入支持:同一个线程重复加锁不阻塞,手写需额外维护线程 ID 和重入计数,极易出错
如果你的项目已引入
,直接用
,比反复核对 Lua 脚本安全得多。
逻辑过期方案里,
不带
是故意的
逻辑过期不是不设过期,而是把过期判断从 Redis 移到应用层。典型结构是存一个 JSON:
这时
确实不带
,因为你要它「永不过期」——但必须配合内存淘汰策略(如
),否则内存迟早爆掉。
真正关键的是读取时的双重判断:
先
,解析出
若
,才去抢互斥锁、异步刷新
抢锁失败?直接返回旧数据,不阻塞
这个方案的代价很明确:用户可能看到最多
周期内的脏数据。如果业务容忍不了(比如余额、库存),就别用逻辑过期,老实用互斥锁+强一致性。
GETkeySELECTsynchronizedstatic Object lock = new Object()synchronizedSETNXSET key value EX seconds NXSETNXDELSETNXEXSETNXDELDEL keyThread.sleep(10)Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("lock:user:1001", "uuid-abc", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
User user = db.selectById(1001);
redisTemplate.opsForValue().set("user:1001", toJson(user), 3600, TimeUnit.SECONDS);
} finally {
// Lua 脚本保证原子性删除
redisTemplate.execute(delLockScript, Collections.singletonList("lock:user:1001"), "uuid-abc");
}
}RedissontryLock业务查询 DB + 写缓存RedissontryLocksleeptryLock(3, 10, TimeUnit.SECONDS)redisson-spring-boot-starterRLock lock = redissonClient.getLock("lock:user:1001")SETEX{"data": {"id": 1001, "name": "Alice"}, "expireTime": 1743825600000}SET user:1001 '{...}'EXmaxmemory-policy allkeys-lruGET user:1001expireTimeSystem.currentTimeMillis() > expireTimeexpireTime