说实话,这个点我一开始也没当回事。
直到有次线上半夜告警:同一笔订单被两个线程同时处理了。排查一圈,最后定位到一个让我后背发凉的原因——持有 Redis 分布式锁的线程,正好撞上了一次 Full GC,看门狗没来得及续期,锁悄无声息地过期了,另一个实例把锁抢走了。
这事儿在面试里也特别容易被追问。今天就把 Redis 锁的原理、GC 怎么把它搞崩、以及它到底有多容易发生,从头到尾拆一遍。
先说结论,省得你划走
GC 停顿确实能让 Redis 锁的看门狗续期失败,这是分布式锁圈子里一个经典的理论弱点(Martin Kleppmann 怼 Redlock 的核心论点之一)。
但它是个低概率、高危害的尾部风险:在调优良好的现代 Java 服务里,平时基本碰不到;一旦碰到,后果是“两个线程同时拿到锁”,直接数据错乱。
划重点:Redlock(红锁)也救不了它。后面会解释为什么。
Redis 分布式锁到底是怎么工作的
要搞懂“为什么会被 GC 搞崩”,得先知道锁长什么样。
最基础的加锁,就是一条命令:
SET lock:order:1001"UUID:线程ID"NX PX30000NX:不存在才设置,保证互斥;PX 30000:30 秒自动过期,防死锁;UUID:线程ID作为 value:解锁时先比对 value,确认是自己的锁才删,避免误删别人的锁。
解锁必须用 Lua 脚本,把“判断 value + 删除”打包成原子操作:
ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0end到这里,一个“能用的”锁就有了。但问题来了。
整体流程可以用一张图串起来:
锁过期了,业务还没跑完怎么办
假设锁 30 秒过期,可你的业务要跑 40 秒。第 30 秒锁被 Redis 自动删了,别的服务进来,两个线程同时操作同一份数据——前面的互斥全白干了。
Redisson 的解法是看门狗(Watchdog)。
它的逻辑很直白:你拿锁成功后,后台起一个定时任务,每隔一段时间就给锁"续命",把过期时间重新刷回 30 秒,直到你主动释放锁。
敲黑板,Redisson 看门狗有三个关键点,面试必考:
- 默认锁租约 30 秒,每10 秒(租约的 1/3)续期一次;
- 续期是用Netty 定时任务发的 Lua 脚本,重新
PEXPIRE; - 只有你不传 leaseTime 时才生效。你要是写了
lock.lock(10, TimeUnit.SECONDS),看门狗直接罢工,10 秒一到锁必定释放,不管你业务跑没跑完。
看门狗的工作机制如下:
GC 是怎么把看门狗搞瘫的
看门狗本质上是 JVM 里的一个定时任务线程。它要干活,前提是你这台机器的 JVM 得“活着”且“能调度”。
而 Full GC 会触发STW(Stop-The-World)停顿。
STW 期间,所有 Java 线程都被冻结——包括你的业务线程,也包括看门狗那个定时任务线程。Redis 那边可不知道你这边卡住了,到点就把锁过期删除。等 GC 结束,业务线程“复活”,它还以为自己握着锁继续写,可实际上锁早没了,别的实例已经进来了。
两个线程同时进临界区,数据错乱就这么来的。
血泪教训:这不是我编的。Redisson 官方文档、Kleppmann 的《How to do distributed locking》都点名过这个场景。
GC 停顿导致锁失效的完整过程:
那到底要多长的 GC 停顿才会出问题
这才是大家最关心的:“实际场景里概率大吗?”
先算笔账。看门狗每 10 秒续一次,每次把 TTL 刷回 30 秒。所以任意时刻,锁的剩余存活时间其实在20~30 秒之间波动。
要让锁真的过期被抢走,STW 停顿必须超过当前剩余 TTL,也就是大致要>20 秒的停顿,而且还得卡在续期窗口附近。
所以门槛其实很高:不是普通的 GC 停顿,而是一次 20 秒级别以上的“真·Full GC”或者系统级停顿。
不同 GC 选型,差别巨大:
- ZGC / Shenandoah(JDK 11+/17+):停顿控制在10 毫秒以内,跟 20 秒差着三个数量级。这个场景基本不可能发生。
- G1(JDK 9+ 默认,调优正常):常规 young/mixed GC 才 10~200 毫秒;真正触发 “Full GC” 是 G1 退化兜底(疏散失败、巨型对象、元空间耗尽),能拖到几十秒,但属于异常事件,不是常态。
- CMS / Parallel(老版本或误配,大堆):Full GC 停顿随堆大小线性增长,几十秒是常事。2026 年已很少见,但存量系统里还有。
还有个容易忽略的点:GC 不是唯一凶手。容器里的 CPU 限流(cgroup throttle)、嘈杂邻居、VM 热迁移,都能造成秒到十秒级的停顿,效果跟 GC 一模一样。
为什么 Redlock 也救不了
很多人下意识觉得:“那我上红锁,5 个 Redis 节点,总稳了吧?”
救不了。因为停顿发生在你的业务客户端 JVM,不在 Redis 那边。你一卡住,5 个节点上的锁会同时在你这边视角里过期——红锁只是多了几个节点,对“客户端被暂停”这件事无能为力。
这正是 Kleppmann 的核心批判:Redlock 依赖“进程不会被长时间打断”的假设,而这个假设在真实环境里站不住脚。Redlock 提升的是可用性,不是这个场景下的安全性。
概率到底大不大
直接给判断:
- 用ZGC/Shenandoah:概率≈0,从根上消除了长停顿;
- 用调优良好的G1 微服务:稳态下低,通常只在故障期扎堆出现(流量突增→分配暴涨→GC 压力→长停顿);
- 老CMS/Parallel + 大堆误配:相对更可能,尤其在高负载时。
总结一句:正常运维的现代化服务里,触发概率不高;但它会在你最倒霉(故障)的时候跳出来,一跳就是数据级事故。
工程上怎么防
别慌,能防,而且不复杂。
第一,选对 GC。新服务直接上 ZGC 或 Shenandoah,长停顿从源头消失。
第二,关键区要短。分布式锁千万别包住长耗时的远程调用、大事务。锁里只做最必要的临界操作,剩下的异步搞。
第三,别把 Redis 锁当“正确性”保证。它更适合“效率型”用途(防重复执行、抢 Leader)。对扣库存、转账这种正确性敏感的场景,必须有兜底。
第四,上 Fencing Token(栅栏令牌)。这是真正的解法:每次加锁拿到一个单调递增令牌,写数据时带上,存储层拒绝比已见令牌更旧的写入。就算旧持有者 GC 后“复活”来写,也被一脚踢开。ZooKeeper(zxid)、etcd(mod-revision)天生带这个;Redis 本身不提供。
第五,监控报警。盯 GC 停顿时间、Full GC 次数。长停顿就是事故前兆,比事后查数据错乱便宜多了。
面试要是被问到,这么答
“GC 停顿会让看门狗续期失败,这是分布式锁的已知理论弱点,本质是要求客户端停顿不超过锁 TTL。实际中 Redisson 每 10 秒续期、锁剩 20~30 秒,要触发需要 20 秒以上的 STW,现代 GC 下稳态概率很低,属于故障期的尾部风险;而且 Redlock 因为停顿在客户端侧也救不了。所以关键数据场景我会加 fencing token 或 DB 约束兜底,而不是只依赖 TTL 锁。”
这段把机制、量化概率、Redlock 局限全点到了,比干巴巴说“会失败”有说服力得多。
写在最后
分布式锁不是银弹。它能解决“大部分时候只有一个线程干活”的效率问题,但扛不住"客户端被冻住几十秒"这种极端情况。
我的做法一直很简单:能用 Fencing Token 兜底的地方绝不裸奔,能用 ZGC 的地方不纠结。真出了事,你才会感谢当初多写的那个令牌校验。
有问题欢迎评论区交流,下篇打算聊聊 Redisson 看门狗的源码细节,感兴趣的点个关注。