
说实话这个点我一开始也没当回事。直到有次线上半夜告警同一笔订单被两个线程同时处理了。排查一圈最后定位到一个让我后背发凉的原因——持有 Redis 分布式锁的线程正好撞上了一次 Full GC看门狗没来得及续期锁悄无声息地过期了另一个实例把锁抢走了。这事儿在面试里也特别容易被追问。今天就把 Redis 锁的原理、GC 怎么把它搞崩、以及它到底有多容易发生从头到尾拆一遍。先说结论省得你划走GC 停顿确实能让 Redis 锁的看门狗续期失败这是分布式锁圈子里一个经典的理论弱点Martin Kleppmann 怼 Redlock 的核心论点之一。但它是个低概率、高危害的尾部风险在调优良好的现代 Java 服务里平时基本碰不到一旦碰到后果是“两个线程同时拿到锁”直接数据错乱。划重点Redlock红锁也救不了它。后面会解释为什么。Redis 分布式锁到底是怎么工作的要搞懂“为什么会被 GC 搞崩”得先知道锁长什么样。最基础的加锁就是一条命令SET lock:order:1001UUID:线程IDNX PX30000NX不存在才设置保证互斥PX 3000030 秒自动过期防死锁UUID:线程ID作为 value解锁时先比对 value确认是自己的锁才删避免误删别人的锁。解锁必须用 Lua 脚本把“判断 value 删除”打包成原子操作ifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0end到这里一个“能用的”锁就有了。但问题来了。整体流程可以用一张图串起来是否是否客户端请求加锁SET lock NX PX 30000是否设置成功获得锁执行业务锁已被占用等待重试业务执行完毕Lua 脚本校验 valuevalue 是否匹配DEL 删除锁锁已过期或非本人持有不删除释放锁完成锁过期了业务还没跑完怎么办假设锁 30 秒过期可你的业务要跑 40 秒。第 30 秒锁被 Redis 自动删了别的服务进来两个线程同时操作同一份数据——前面的互斥全白干了。Redisson 的解法是看门狗Watchdog。它的逻辑很直白你拿锁成功后后台起一个定时任务每隔一段时间就给锁续命把过期时间重新刷回 30 秒直到你主动释放锁。敲黑板Redisson 看门狗有三个关键点面试必考默认锁租约 30 秒每10 秒租约的 1/3续期一次续期是用Netty 定时任务发的 Lua 脚本重新PEXPIRE只有你不传 leaseTime 时才生效。你要是写了lock.lock(10, TimeUnit.SECONDS)看门狗直接罢工10 秒一到锁必定释放不管你业务跑没跑完。看门狗的工作机制如下Redis看门狗线程业务线程Redis看门狗线程业务线程loop[每 10 秒]加锁成功TTL30s启动定时续期任务PEXPIRE 刷新 TTL30s续期成功业务完成释放锁停止续期任务GC 是怎么把看门狗搞瘫的看门狗本质上是 JVM 里的一个定时任务线程。它要干活前提是你这台机器的 JVM 得“活着”且“能调度”。而 Full GC 会触发STWStop-The-World停顿。STW 期间所有 Java 线程都被冻结——包括你的业务线程也包括看门狗那个定时任务线程。Redis 那边可不知道你这边卡住了到点就把锁过期删除。等 GC 结束业务线程“复活”它还以为自己握着锁继续写可实际上锁早没了别的实例已经进来了。两个线程同时进临界区数据错乱就这么来的。血泪教训这不是我编的。Redisson 官方文档、Kleppmann 的《How to do distributed locking》都点名过这个场景。GC 停顿导致锁失效的完整过程实例 B抢锁Redis实例 A 的 JVM实例 A持有锁实例 B抢锁Redis实例 A 的 JVM实例 A持有锁所有线程冻结看门狗无法续期两个实例同时进入临界区数据错乱加锁成功TTL30s触发 Full GCSTW 停顿30s 到期锁自动过期删除SET NX 成功抢到锁GC 结束线程恢复业务继续执行以为还持有锁那到底要多长的 GC 停顿才会出问题这才是大家最关心的“实际场景里概率大吗”先算笔账。看门狗每 10 秒续一次每次把 TTL 刷回 30 秒。所以任意时刻锁的剩余存活时间其实在20~30 秒之间波动。要让锁真的过期被抢走STW 停顿必须超过当前剩余 TTL也就是大致要20 秒的停顿而且还得卡在续期窗口附近。所以门槛其实很高不是普通的 GC 停顿而是一次 20 秒级别以上的“真·Full GC”或者系统级停顿。不同 GC 选型差别巨大ZGC / ShenandoahJDK 11/17停顿控制在10 毫秒以内跟 20 秒差着三个数量级。这个场景基本不可能发生。G1JDK 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 后“复活”来写也被一脚踢开。ZooKeeperzxid、etcdmod-revision天生带这个Redis 本身不提供。第五监控报警。盯 GC 停顿时间、Full GC 次数。长停顿就是事故前兆比事后查数据错乱便宜多了。面试要是被问到这么答“GC 停顿会让看门狗续期失败这是分布式锁的已知理论弱点本质是要求客户端停顿不超过锁 TTL。实际中 Redisson 每 10 秒续期、锁剩 20~30 秒要触发需要 20 秒以上的 STW现代 GC 下稳态概率很低属于故障期的尾部风险而且 Redlock 因为停顿在客户端侧也救不了。所以关键数据场景我会加 fencing token 或 DB 约束兜底而不是只依赖 TTL 锁。”这段把机制、量化概率、Redlock 局限全点到了比干巴巴说“会失败”有说服力得多。写在最后分布式锁不是银弹。它能解决“大部分时候只有一个线程干活”的效率问题但扛不住客户端被冻住几十秒这种极端情况。我的做法一直很简单能用 Fencing Token 兜底的地方绝不裸奔能用 ZGC 的地方不纠结。真出了事你才会感谢当初多写的那个令牌校验。有问题欢迎评论区交流下篇打算聊聊 Redisson 看门狗的源码细节感兴趣的点个关注。