Redisson分布式锁实战:可重入、看门狗续期与锁超时调优

发布时间:2026/10/26 20:42:17
Redisson分布式锁实战:可重入、看门狗续期与锁超时调优 分布式锁这个话题在后台服务里出现的频率极高几乎每个做过多实例部署的团队都会碰到。我最早接触这块是在一个订单履约系统里当时两个节点同时处理同一笔退款账目直接对不上排查了大半天才定位到是并发写冲突。后来试过基于数据库唯一索引、基于缓存原子命令自己手写、基于临时有序节点等几种方案兜兜转转最后在多数业务场景里落到了 Redisson 上。这篇就围绕Redisson 实现分布式锁这个主题把我这些年踩过的坑、调过的参数、想明白的原理尽量完整地摊开讲一遍。先说清楚这篇适合谁看如果你已经有多实例部署的服务正在为“同一份资源被两个进程同时改”发愁或者你已经在用 Redisson 但只是lock()一把梭、没深究过续期和可重入那这篇会对你有用。如果你还没接触过分布式协调也没关系我会把加锁的本质、为什么单机锁不够用这些前置知识用生活化的方式讲明白再往深里走。核心关键词会围绕分布式锁、可重入、看门狗续期、锁超时、红锁这几个点展开读到后面你会发现它们其实是环环相扣的一整套设计。1. 先把问题想透为什么单机锁在分布式场景下会失效1.1 从一把“本地锁”的局限说起单机环境下的锁本质是 JVM 内部的一块内存状态。你用synchronized或者ReentrantLock锁的持有者、等待队列、重入计数全都存在当前进程的堆里。同一个进程内的多个线程来抢大家看的是同一份状态所以能互斥。可一旦服务扩成两个实例A 实例的锁状态在 B 实例的内存里根本不存在B 的线程压根不知道 A 已经锁住了于是两边同时进临界区互斥就破了。打个生活化的比方单机锁像是你家卫生间的门锁家里人排队用没问题分布式锁则像是公司茶水间唯一一台咖啡机的使用牌牌子得放在一个所有人都能看到的地方谁拿到牌子谁用用完挂回去。这个“所有人都能看到的地方”就是分布式锁的存储载体通常是一块所有节点都能访问的共享存储。所以分布式锁要解决的核心问题就三个互斥同一时刻只有一个持有者、可见性所有节点都能看到锁状态、可靠性持锁节点挂了锁不能永远不释放。这三个词看着简单真正难的是在网络分区、节点宕机、时钟漂移这些异常情况下还能尽量守住它们。1.2 自研分布式锁常见的三个坑在落到 Redisson 之前很多团队会先自己基于缓存的原生命令手写一版SET key value NX PX 30000这种写法网上一搜一大把。我早期也这么干过结果踩了三个典型的坑正好说明为什么后来要换成熟组件。第一个坑是误删别人的锁。A 拿到锁后业务执行超时锁自动过期了B 紧接着拿到锁这时 A 执行完去DEL key把 B 的锁给删了。解决办法是 value 存一个唯一标识比如 UUID 加线程 ID删除前先比对。但“比对再删除”这两步不是原子的中间还是可能插进别的操作得靠 Lua 脚本把比对和删除打包成一个原子动作。第二个坑是锁过期但业务没执行完。你设了 30 秒过期结果某次数据库慢查询拖了 40 秒锁提前释放互斥失效。有人会说那就把过期时间设长点设成 5 分钟。可一旦持锁节点真的宕机锁要 5 分钟才释放这期间所有请求全堵着可用性又崩了。过期时间短了不安全、长了不灵活这个矛盾靠固定值根本解不开必须有“业务还活着就自动续期”的机制。第三个坑是不可重入。同一个线程在方法 A 里加了锁调用方法 B 时又加同一把锁如果实现不支持重入自己就把自己锁死了。业务代码里这种嵌套调用太常见了所以可重入几乎是刚需。这三个坑恰好对应了 Redisson 的三个核心能力唯一标识加 Lua 原子释放、看门狗自动续期、基于 Hash 结构的可重入计数。理解了坑再看它的设计就会觉得顺理成章。1.3 Redisson 在方案选型中的位置市面上的分布式锁方案大致分几类基于关系型数据库唯一索引或行锁、基于缓存原子命令加 Lua、基于协调服务临时有序节点。数据库方案实现简单但性能一般且锁释放依赖应用主动操作宕机后容易残留协调服务方案一致性较好但引入的组件重、运维成本高缓存方案性能高、部署轻是大多数互联网业务的首选。Redisson 就是缓存方案里封装得比较完整的一个客户端。它把加锁、释放、续期、重入、等待唤醒这些细节都包进了RLock接口里底层用 Lua 脚本保证原子性用发布订阅做锁释放的通知用后台线程做续期。选它的理由很实际不用自己维护那一堆 Lua 脚本和边界条件出问题的概率低。当然封装得深也意味着你得知道它内部在干什么否则参数配错了照样出事这也是后面要重点拆的部分。2. 核心机制拆解Redisson 到底在背后做了什么2.1 加锁的原子性与 Hash 结构设计Redisson 的锁在缓存里不是存一个简单的字符串而是一个 Hash 结构。key 是锁的名字field 是“客户端唯一标识 线程 ID”value 是重入次数。为什么这么设计因为要同时满足两个需求一是能区分“这把锁是谁持有的”二是能记录“同一个持有者重入了几次”。加锁时执行一段 Lua 脚本逻辑大致是先判断 key 是否存在不存在就直接写入 field 和计数 1并设置过期时间存在的话判断 field 是不是自己的是自己的就把计数加一不是自己的就返回当前剩余过期时间让调用方知道还要等多久。整段脚本在缓存服务端原子执行避免了“判断”和“写入”之间被别的请求插队。这里有个细节值得说过期时间是在第一次加锁时设置的重入时不会重置。也就是说重入只是把计数加一锁的整体存活时间还是从第一次加锁算起。这个设计是有意为之否则一个反复重入的业务可能让锁无限续命掩盖了真正的执行超时问题。理解这一点后面调看门狗参数时就不会困惑。2.2 可重入计数与释放的配对逻辑可重入的实现靠的就是那个计数。线程第一次加锁计数为 1重入一次变 2释放时每调一次unlock()计数减一减到 0 才真正删除 key 并触发释放通知。这就要求加锁和释放必须严格配对加了几次就得解几次。我见过不少事故是lock()之后在某个分支里return了却没unlock()计数永远减不到 0锁只能等过期。所以实操上我一律建议用try/finally把unlock()包起来哪怕你觉得这段代码不可能抛异常。另外要注意unlock()如果在一个没持有锁的线程里调用会直接抛异常这其实是好事能帮你尽早发现配对错误别去 catch 掉它。释放同样走 Lua 脚本判断 field 是不是自己的是就把计数减一减完如果大于 0 就更新计数并续一下过期时间等于 0 就删除 key 并发布一条释放消息。删除和发布消息打包在一个脚本里保证“锁没了”和“通知等待者”这两件事不会脱节。2.3 看门狗续期的触发条件与边界看门狗是 Redisson 最容易被误解的机制。很多人以为只要用了 Redisson 就自动续期其实不是。只有加锁时没有显式指定过期时间也就是调用无参的lock()看门狗才会启动。如果你写的是lock(30, TimeUnit.SECONDS)那就是固定 30 秒过期不会续期到点就释放哪怕业务还在跑。看门狗的默认续期周期是锁过期时间的三分之一默认锁过期时间是 30 秒所以大约每 10 秒续一次每次把过期时间重置回 30 秒。这个“三分之一”的比例是有讲究的续期太频繁会给缓存带来额外压力续期间隔太长又可能在两次续期之间锁就过期了三分之一是个兼顾两者的经验值。但看门狗不是万能的。它依赖持锁进程还活着、还能和缓存通信。如果进程假死比如长时间 Full GC或者网络断了续期线程跑不起来锁照样会过期。所以看门狗解决的是“业务执行时间不确定”的问题解决不了“持锁者失联”的问题后者只能靠合理的过期时间兜底。这一点想不清楚就容易在看门狗上寄托过多期望。2.4 等待唤醒发布订阅如何减少空转当锁被占用时等待的线程怎么办最简单的做法是自旋隔一小段时间重试一次。但自旋在锁竞争激烈时会疯狂打缓存既浪费资源又增加延迟。Redisson 用的是发布订阅抢锁失败后当前线程订阅这个锁的释放频道然后阻塞等待持锁者释放时发布消息等待者被唤醒再去抢。这个机制的好处是等待期间不消耗 CPU 和网络。但它也有个边界订阅关系是基于连接的如果等待期间连接断了唤醒消息可能收不到。Redisson 对此做了兜底等待是有超时时间的超时后会重新走一遍抢锁流程不会无限挂死。理解这个机制你就明白为什么锁释放后等待者不是“立刻”拿到锁而是有个被唤醒再竞争的过程高并发下这个微小延迟是正常的。3. 上手实操从依赖引入到一把可靠的锁3.1 环境准备与客户端配置先把依赖引进来。以 Maven 为例加一个 Redisson 的 starter版本建议选和你的缓存服务版本兼容的稳定版别盲目追最新。引入后要做的是配置客户端这一步很多人直接抄个单机地址就完事其实有几个参数值得认真设。singleServerConfig: address: redis://127.0.0.1:6379 connectionMinimumIdleSize: 8 connectionPoolSize: 32 timeout: 3000 retryAttempts: 3 retryInterval: 1500connectionPoolSize要根据你的并发量估锁操作本身很快但连接不够时请求会排队表现为“莫名其妙变慢”。timeout是命令超时设太短会在缓存抖动时频繁抛异常设太长又会让故障感知变迟钝3000 毫秒是个常见的起点。retryAttempts和retryInterval控制重试注意这里的重试是针对连接层面的和业务层的抢锁重试不是一回事别混淆。配置客户端时还要想清楚用单机、主从还是集群模式。单机最简单但没高可用主从在主节点宕机、从节点晋升的瞬间如果锁还没同步过去可能出现两个持有者这是异步复制的固有风险集群模式把锁分散到不同分片单分片故障影响面小一些。选哪种取决于你对一致性和可用性的权衡没有银弹。3.2 三种加锁方式的取舍Redisson 的RLock提供了几种加锁入口用哪种直接决定了出问题时的表现。第一种是lock()无参调用阻塞直到拿到锁拿不到就一直等同时启动看门狗续期。适合“必须拿到锁才能继续、且业务执行时间不确定”的场景。风险是如果锁被一个失联的节点长期占着虽然会过期但过期前一直等调用线程会一直阻塞可能拖垮线程池。第二种是lock(leaseTime, unit)指定过期时间不续期。适合“业务执行时间可控、能明确估出上限”的场景。好处是即使持锁者挂了锁最多占用 leaseTime 就释放不会长期阻塞。代价是你得对业务耗时心里有数估短了会提前释放。第三种是tryLock(waitTime, leaseTime, unit)带等待时间和过期时间拿不到锁等 waitTime 就返回 false不无限阻塞。这是我在生产里用得最多的一种因为它把“等多久”和“持多久”都显式化了行为可预期。RLock lock redissonClient.getLock(order:refund: orderId); boolean acquired false; try { acquired lock.tryLock(3, 10, TimeUnit.SECONDS); if (!acquired) { // 没抢到锁走降级或直接返回 return Result.busy(); } // 临界区业务 doRefund(orderId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (acquired lock.isHeldByCurrentThread()) { lock.unlock(); } }这段代码有两个关键点isHeldByCurrentThread()判断当前线程是否真的持有锁避免在没持有时调unlock()抛异常acquired标志确保只有抢到锁才释放。别小看这两行线上很多“锁释放异常”的告警就是漏了它们。3.3 锁粒度与 key 设计锁的粒度直接决定并发度。锁太粗比如用一个大 key 锁住所有订单操作那所有请求串行吞吐直接崩锁太细比如每个字段一把锁又会让业务逻辑复杂到难以维护。我的经验是按业务聚合根来定粒度订单退款就锁单个订单 ID库存扣减就锁单个商品 ID这样同一资源的操作互斥不同资源并行。key 的命名要有统一规范建议用“业务域:资源类型:资源 ID”的格式比如order:refund:12345。统一前缀的好处是排查时能快速定位也方便做监控和清理。千万别用会动态变化、带随机数的 key那样锁根本起不到互斥作用还查不出问题。还有一个容易忽略的点锁的 key 和业务数据的 key 不要混用同一个命名空间。曾经有个项目把锁 key 和缓存数据 key 用了相同前缀结果清理缓存时把锁也删了互斥瞬间失效。这种问题排查起来极其痛苦提前规范命名就能避免。3.4 释放锁的正确姿势释放锁看着简单其实讲究不少。前面说了要用try/finally加isHeldByCurrentThread()判断这里再补两个细节。一是不要在 finally 里吞异常。unlock()本身可能因为连接问题抛异常如果你 catch 了不处理锁可能没真正释放只能等过期。合理的做法是记录日志并告警让运维知道有锁释放失败而不是静默吞掉。二是释放要及时。临界区代码执行完立刻释放别把日志打印、远程调用这些耗时操作放在锁里面。我见过把一次 HTTP 调用放在锁内的结果锁持有时间从毫秒级涨到秒级并发能力断崖式下跌。原则是锁内只放必须互斥的最小逻辑其他都挪到锁外。4. 参数调优与异常场景那些文档里不会写的经验4.1 过期时间到底怎么估过期时间设多少是分布式锁里最需要动脑子的参数。设太短业务没跑完锁就没了互斥失效设太长持锁者挂了之后锁长期不释放其他请求全堵着。我的估算方法是先测出临界区业务在 P99 分位下的耗时再乘以 2 到 3 的安全系数。举个例子某次退款业务 P99 耗时 800 毫秒那 leaseTime 设 3 秒左右比较稳妥既覆盖了绝大多数情况又不会在异常时占用太久。如果你用了看门狗无参lock()那过期时间其实是“续期周期”的基准默认 30 秒够用但如果你把默认值改小了要确保续期周期仍然大于业务单次执行时间否则续期还没到锁就过期了。这里有个反直觉的点不是所有场景都适合看门狗。如果你的业务耗时稳定且短用固定 leaseTime 反而更可控因为锁的释放时间是可预期的不会出现“业务卡住导致锁一直被续期”的情况。看门狗适合的是“耗时波动大、难以预估上限”的场景比如涉及外部依赖调用的批处理。4.2 锁等待超时与线程池的联动tryLock的 waitTime 设多少要和你的线程池配置一起考虑。假设线程池有 50 个线程waitTime 设 10 秒那在最坏情况下50 个线程可能同时阻塞 10 秒等锁这期间线程池没有余力处理其他请求可能引发级联超时。我的做法是waitTime 设短一些比如 1 到 3 秒抢不到就快速失败走降级。降级可以是返回“系统繁忙请重试”也可以是把请求丢进队列稍后处理具体看业务。快速失败的好处是保护线程池避免一个热点资源的锁竞争拖垮整个服务。这个思路和“超时时间要短、失败要快”的通用原则是一致的。另外要监控抢锁失败率这个指标。如果某个锁的失败率持续偏高说明要么锁粒度太粗、要么临界区执行太慢、要么就是有死锁或锁泄漏。这个指标比单纯的 QPS 更能反映锁的健康度建议接入监控大盘。4.3 主从切换下的锁安全边界前面提过主从模式下锁存在一个理论上的不安全窗口主节点写入锁后还没同步到从节点就宕机了从节点晋升后不知道这把锁存在另一个客户端就能拿到同一把锁。这个窗口在异步复制下是客观存在的Redisson 的红锁RedLock算法就是为缓解这个问题设计的——它要求同时在多个独立的缓存节点上加锁超过半数成功才算拿到锁。但红锁也不是银弹。它对时钟、网络延迟敏感实现和运维复杂度都高而且在极端网络分区下仍有争议。我的实际取舍是如果业务对锁的安全性要求极高比如涉及资金不要只依赖缓存锁要在数据层再加一道乐观锁或唯一约束做最终兜底。缓存锁负责挡住绝大多数并发数据层的约束负责兜住极端情况两层配合才稳。单纯指望某一种锁方案做到绝对安全是不现实的。4.4 常见问题速查表把这些年遇到的高频问题整理成一张表方便对照排查。现象可能原因排查方向处理建议锁释放时报“未持有锁”异常加锁释放不配对或跨线程释放检查是否在异步线程里 unlock用 try/finally 配对加 isHeldByCurrentThread 判断业务没执行完锁就没了用了固定 leaseTime 且估短了看日志里锁过期时间与业务耗时改用看门狗或调大 leaseTime锁一直不释放持锁进程假死看门狗续期线程卡住查进程 GC 日志、线程栈设合理 leaseTime 兜底加锁泄漏监控抢锁失败率异常高锁粒度过粗或临界区太慢看锁 key 分布和临界区耗时细化锁粒度缩短临界区主从切换后出现双持有者异步复制窗口查切换时间点与锁操作时间数据层加乐观锁兜底或评估红锁等待锁的线程大量阻塞waitTime 设太长看线程池活跃数与阻塞数缩短 waitTime快速失败降级这张表不是穷举但覆盖了八成以上的线上问题。遇到新问题先对照现象找方向再去翻 Redisson 的日志和缓存里的锁结构通常能定位。5. 把锁用对几个容易被忽视的工程习惯5.1 锁的监控与可观测性锁是“看不见的并发控制”出了问题往往表现为业务数据错乱而不是明显的报错所以监控尤其重要。我一般会关注几个指标加锁成功率、平均持锁时长、锁等待队列长度、续期次数。加锁成功率骤降说明竞争加剧或有锁泄漏持锁时长异常增长说明临界区变慢续期次数异常说明有业务长时间占锁。实现上可以在RLock外面包一层记录加锁前后时间戳和结果上报到监控系统。别嫌麻烦等真出问题时这些数据能帮你把排查时间从几小时缩短到几分钟。我经历过一次数据错乱最后就是靠持锁时长曲线发现某类请求的锁持有时间突然从几十毫秒涨到十几秒顺藤摸瓜找到了一个慢查询。5.2 临界区代码的编写纪律锁用对了临界区写错了照样出事。几条纪律值得贴在工位上临界区内不做远程调用、不打印大日志、不睡眠等待、不嵌套获取其他锁。远程调用会让持锁时间不可控大日志会拖慢执行睡眠纯属浪费锁资源嵌套获取多把锁则是死锁的温床。如果业务确实需要在一个逻辑里操作多个资源尽量按固定顺序获取锁比如都按资源 ID 升序加锁这样能避免两个线程交叉持锁导致的死锁。这个技巧在多资源操作的场景里非常实用成本低效果好。5.3 什么时候不该用分布式锁最后说个反向的经验不是所有并发问题都该用分布式锁解决。如果能把操作设计成幂等的很多场景根本不需要锁如果能用数据库的乐观锁版本号或唯一约束解决也不必引入缓存锁如果并发量很低用数据库行锁可能更简单可靠。分布式锁是有成本的多一次网络往返、多一个故障点、多一份运维负担。我见过为了一个每天只跑几次的定时任务加分布式锁的其实用数据库的唯一约束或者调度框架自带的分片就能解决。先问“能不能不用锁”再问“用哪种锁”这个顺序能帮你省掉很多不必要的复杂度。真正需要分布式锁的是那些“必须严格互斥、且互斥逻辑跨进程”的场景比如同一订单的并发退款、同一库存的并发扣减、同一资源的并发初始化。这些场景下Redisson 提供的这套机制确实能省不少事前提是你理解了它的边界知道它在什么情况下会失效并做好了兜底。我个人在实际操作中的体会是分布式锁的难点从来不在“怎么加锁”而在“想清楚锁的边界和失效后的兜底”。Redisson 把加锁这件事封装得很好但续期、过期、主从切换这些边界条件还是得靠使用者自己判断和配置。把参数估准、把监控做好、把数据层的兜底加上这套组合拳打下来锁才算真正用稳了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询