直接用setnx实现互斥锁需搭配px过期时间、唯一锁值(如uuid)及lua脚本原子释放,否则无法解决缓存击穿;synchronized仅作用于单jvm,多实例下完全无效。

直接用 SETNX 实现互斥锁能解决缓存击穿,但多数人漏掉三个关键动作:抢锁后没二次查缓存、锁值没做客户端标识、释放锁没用 Lua 脚本校验。这三处一错,集群环境下照样击穿。
为什么单机 synchronized 在 Redis 击穿场景下完全无效
Java 的 synchronized 只锁当前 JVM 进程,而线上服务基本都是多实例部署。哪怕你写成 if (cache == null) { synchronized(this) { ... } },10 台机器每台都会各自执行一次 DB 查询——10 万并发进来,DB 照样被压垮 10 次。
更危险的是 volatile 变量,它连“读-改-写”的原子性都不保证,根本不能当锁用。分布式环境里,唯一可信的协调者是 Redis 本身。
- 集群中每个节点都独立运行,本地锁对其他节点毫无约束力
- 即使压测时单机表现正常,上线后必然复现击穿
- 真正起作用的不是“加锁”这个动作,而是“所有节点都向同一个 Redis 实例申请同一把锁”
SETNX 必须搭配 PX 和唯一锁值才能用
SETNX 单独用等于裸奔。必须用 SET key value NX PX 30000 这种原子命令替代分步的 SETNX + EXPIRE,否则中间出错(比如网络中断、进程崩溃)会导致锁永远不释放。
锁值也不能硬写成 "1" 或 "true",得是每个客户端生成的唯一标识(如 UUID),否则 A 拿到锁还没写完,B 就误删了锁,A 回填时发现锁没了,可能重复写入或丢数据。
- 推荐命令:
SET mutex:goods:1001 <uuid> NX PX 30000</uuid> - 锁 key 名建议带业务前缀,避免不同资源共用一个锁名导致误删
- PX 时间要明显大于 DB 查询耗时(比如 DB 平均 200ms,锁设 30s),但不能过长(超 60s 易引发等待风暴)
抢到锁之后一定要再查一次缓存
这是最容易被跳过的一步。线程 A 抢到锁、去查 DB、写缓存、释放锁;线程 B 在 A 释放锁前就轮询到了,此时缓存已存在,B 应该直接返回,而不是再走一遍锁流程。
所以标准流程是:查缓存 → miss → 尝试 SET 加锁 → 成功则**再次查缓存** → 仍 miss 才查 DB → 写缓存 → 释放锁;失败则休眠后重试。
- 伪代码关键判断:
if (redis.get(key) != null) { return value; }必须出现在加锁成功后的第一时间 - 空值也要缓存(比如
SET goods:1001 "NULL" EX 60),否则穿透和击穿会一起爆发 - 休眠时间别设太短(如
Thread.sleep(10)),50–100ms 更稳妥,避免 Redis 频繁收 SET/PX 请求
Lua 脚本释放锁是底线,不能省
用 DEL mutex:goods:1001 删除锁?只要服务在释放锁前崩溃,或者锁超时被自动清除,就可能让别的线程误删正在使用的锁。必须用 Lua 脚本做原子校验:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这段脚本确保只有加锁者才能删锁,且整个“判断+删除”不可分割。任何绕过它的释放方式(比如先 GET 再 DEL)在高并发下都不可靠。
真正卡住人的从来不是锁怎么加,而是锁怎么安全地放回去。生产环境里,90% 的锁相关故障都出在释放环节——要么没校验值,要么没用原子脚本,要么忘了放在 finally 块里。











