缓存击穿需用分布式锁而非synchronized,redis setnx+ex+校验value+退避重试是手写底线;redisson因看门狗续期、阻塞等待、可重入更可靠;逻辑过期靠应用层判断expiretime并容忍脏数据。

缓存击穿发生时,GET 返回 null 后直接查 DB 是最危险的路径
缓存击穿本质是:一个高并发访问的热点 key 刚好过期,大量请求同时发现缓存未命中,全部涌向数据库。此时如果每个请求都独立执行 SELECT,DB 瞬间被打满,连接池耗尽、慢 SQL 堆积、服务雪崩。
很多人第一反应是加 synchronized,比如用 static Object lock = new Object() 包住 DB 查询逻辑。但这只在单 JVM 进程内有效——现代微服务基本都是多实例部署,synchronized 对其他机器上的线程完全无效,锁形同虚设。
真正要拦住的是「跨进程、跨机器」的并发请求,必须靠分布式协调机制。Redis 的 SETNX(或更安全的 SET key value EX seconds NX)天然适合做这层拦截,因为它由 Redis 单点原子性保障,所有客户端都服从同一把锁。
SETNX + DEL 手动实现分布式锁的三个硬约束
不用框架也能快速落地,但必须守住三条底线,否则等于没锁:
-
SET必须带NX(不存在才设置)和EX(自动过期),缺一不可。只写SETNX不设超时,一旦业务异常没释放锁,就永久死锁 -
DEL删除锁前必须校验 value 是否匹配(防止 A 加锁、B 超时释放了 A 的锁),纯DEL key是严重误删 - 获取锁失败后不能无限自旋,必须
Thread.sleep(10)或指数退避,否则千个线程空转 CPU,服务先于 DB 崩溃
示例关键逻辑:
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");
}
}
为什么 Redisson 的 tryLock 比手写更可靠
手写锁容易漏掉几个关键细节:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 锁续期问题:
业务查询 DB + 写缓存耗时超过 10 秒,而锁已过期,其他线程趁虚而入 ——Redisson的看门狗(watchdog)会自动给未释放锁续命 - 阻塞等待逻辑:手写需自己循环
tryLock+sleep,而tryLock(3, 10, TimeUnit.SECONDS)直接封装了「最多等 3 秒,锁最长持有 10 秒」的语义 - 可重入支持:同一个线程重复加锁不阻塞,手写需额外维护线程 ID 和重入计数,极易出错
如果你的项目已引入 redisson-spring-boot-starter,直接用 RLock lock = redissonClient.getLock("lock:user:1001"),比反复核对 Lua 脚本安全得多。
逻辑过期方案里,SET 不带 EX 是故意的
逻辑过期不是不设过期,而是把过期判断从 Redis 移到应用层。典型结构是存一个 JSON:
{"data": {"id": 1001, "name": "Alice"}, "expireTime": 1743825600000}
这时 SET user:1001 '{...}' 确实不带 EX,因为你要它「永不过期」——但必须配合内存淘汰策略(如 maxmemory-policy allkeys-lru),否则内存迟早爆掉。
真正关键的是读取时的双重判断:
- 先
GET user:1001,解析出expireTime - 若
System.currentTimeMillis() > expireTime,才去抢互斥锁、异步刷新 - 抢锁失败?直接返回旧数据,不阻塞
这个方案的代价很明确:用户可能看到最多 expireTime 周期内的脏数据。如果业务容忍不了(比如余额、库存),就别用逻辑过期,老实用互斥锁+强一致性。










