缓存击穿需“锁+过期时间随机化+空值缓存”组合防御,而非仅靠加锁;关键在明确锁对象、时长、触发时机及失败兜底,且必须用tryLock()、双重检查、安全解锁。

缓存击穿不是靠加锁就能自动解决的,Redisson 分布式锁只是其中一环;关键在于“谁来触发加锁”“锁住什么”“锁多久”,以及“锁失败后怎么兜底”。
缓存击穿场景下,为什么直接用 RLock.lock() 会失效
缓存击穿指某个热点 key 过期瞬间,大量并发请求同时穿透缓存直查数据库。此时如果每个请求都各自调用 RLock.lock() 去抢锁,会出现两个典型问题:
- 锁粒度错:你锁的是业务资源 ID(比如
"user:1001"),但实际要保护的是“从 DB 加载并回填该 key 到 Redis”这个动作——而这个动作本身没有天然的唯一标识,容易导致多个线程重复执行加载逻辑 - 锁等待策略不合理:默认
lock()是阻塞等待,高并发下大量线程卡在锁上,拖慢响应甚至引发超时雪崩 - 没区分“首次加载”和“后续读取”:锁应该只在缓存为空 + 尝试加载时才需要,而不是每次 get 都去抢锁
正确姿势:用 tryLock() + 双重检查 + 回填原子性
核心是把“查缓存 → 缓存 miss → 加锁 → 查库 → 写缓存 → 解锁”这一整条链路拆解清楚,避免锁被滥用:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 先
redis.get("user:1001"),不为空直接返回,不走锁 - 为空时,调用
lock.tryLock(3, 10, TimeUnit.SECONDS):最多等 3 秒,锁自动续期 10 秒(防止任务超时释放) - 拿到锁后,**再次检查缓存**(双重检查):因为可能你在等锁时,别的线程已加载完并写入了缓存
- 仍为空,才查 DB、
redis.set("user:1001", user, 60, TimeUnit.SECONDS),然后解锁 - 没拿到锁?说明别人正在加载,你就 sleep 50ms 后重试 get,或直接 fallback 返回空/默认值
必须设置 leaseTime 和 waitTime,不能依赖默认值
Redisson 的 RLock 默认锁有效期是 30 秒,且不会自动续期;这对缓存加载类场景极危险——如果 DB 查询慢于 30 秒,锁提前释放,别的线程就会重复加载。
- 显式传参:
lock.tryLock(2, 60, TimeUnit.SECONDS)表示最多等 2 秒,拿到锁后自动续期 60 秒(watchdog 生效) - 不建议用无参
lock():它不设过期时间,依赖 watchdog,而 watchdog 默认只在持有锁的线程存活时才续期;若线程卡顿或 GC 停顿,续期失败就可能误释放 - waitTime 太长(如 10 秒)会让前端响应延迟不可控;太短(如 100ms)又容易频繁失败重试,需按接口 SLA 调整
解锁必须用 unlock(),严禁 del 或脚本硬删
Redisson 的锁 value 是复合结构(含 UUID + 线程 ID + 重入次数),直接 redis.del("lock:user:1001") 会删掉别人的锁,或者删掉自己已续期多次的锁状态。
- 只允许用
lock.unlock(),它内部通过 Lua 脚本比对 value 再删除,安全 - 别在 finally 块里无条件 unlock():如果 tryLock 失败,lock 对象是 null 或未真正持有锁,调用 unlock() 会抛
IllegalMonitorStateException - 更稳妥写法:加个判空
if (lock != null && lock.isHeldByCurrentThread()) { lock.unlock(); }
最容易被忽略的一点:缓存击穿防御不是单靠锁,而是“锁 + 过期时间随机化 + 空值缓存”组合拳。比如 set("user:1001", user, 58 + ThreadLocalRandom.current().nextInt(5), TimeUnit.SECONDS),让 key 失效时间分散,大幅降低击穿概率。锁只是兜底手段,不该是第一道防线。

















