Redisson分布式锁实战:从手写SETNX到生产级方案
发布时间:2026/10/26 22:14:57 锦皓数字建站

分布式系统里只要涉及到多节点共享资源锁这件事就绕不开。单机时代一个synchronized或者ReentrantLock就能搞定到了多实例部署的环境JVM 级别的锁瞬间失效——两个请求打到不同节点上各自拿着自己进程内的锁互相看不见对方超卖、重复扣款、并发写脏数据这类问题就全冒出来了。我最早接触分布式锁是在一个库存扣减的场景里当时用数据库行锁硬扛QPS 一上来数据库直接被打爆后来换成缓存中间件加锁才算稳住。这几年做过的项目里Redisson的分布式锁出场频率相当高它把锁的续期、可重入、阻塞等待这些脏活累活都封装好了用起来比手写 SETNX 舒服太多。这篇就围绕 Redisson 实现分布式锁这件事把我踩过的坑、调过的参数、验证过的方案完整拆一遍适合正在做分布式并发控制、或者准备把现有手写锁迁移到成熟框架的朋友参考不管你是刚接触分布式锁的新手还是想搞清楚 Redisson 内部机制的进阶选手应该都能捞到点东西。1. 为什么选 Redisson 而不是手写 SETNX1.1 手写锁的三个致命短板很多人第一次实现分布式锁都是照着缓存中间件的SET key value NX PX timeout命令来的思路很直白键不存在就写入成功代表拿到锁写入失败代表锁被占用再加个过期时间兜底防止死锁。这个方案能跑通 demo但放到生产环境里问题一个接一个。第一个短板是锁误删。假设 A 线程拿到锁业务执行时间超过了锁的过期时间锁自动释放了这时候 B 线程拿到同一把锁开始干活A 线程执行完回来执行 DEL 操作结果把 B 的锁给删了。解决办法是在 value 里存一个唯一标识删除前先比对但比对和删除这两步不是原子的中间还是可能出岔子得上 Lua 脚本才能保证原子性。第二个短板是续期问题。业务执行时间根本没法预估过期时间设短了容易提前释放设长了万一节点宕机锁要等很久才释放。手写方案里没有自动续期机制只能靠人肉估算一个足够大的过期时间这本身就是个拍脑袋的决定。第三个短板是不可重入。同一个线程嵌套调用两次加锁逻辑第二次直接失败因为键已经存在了。要支持可重入就得在 value 里维护重入计数逻辑复杂度直线上升。注意这三个短板不是靠写得仔细一点就能规避的它们本质上是手写方案缺少原子性保障和生命周期管理导致的属于方案层面的缺陷。1.2 Redisson 把这些坑都填了Redisson 的定位是缓存中间件之上的 Java 驻内存数据网格框架分布式锁只是它众多组件里的一个。它基于 Netty 做通信把锁的加锁、解锁、续期、重入全部封装成 Lua 脚本在服务端原子执行客户端只管调用 API。具体来说它解决了这么几件事可重入靠 Hash 结构实现key 是锁名field 是客户端唯一标识UUID 线程 IDvalue 是重入次数每次加锁计数加一解锁减一减到零才真正删除键自动续期靠看门狗机制默认锁过期 30 秒后台每 10 秒检查一次业务没执行完就重置过期时间阻塞等待靠发布订阅拿不到锁的线程订阅解锁消息锁释放时被唤醒重试而不是无脑自旋空转。这套机制下来业务代码只需要lock.lock()和lock.unlock()两行剩下的全交给框架。我个人的判断是除非你有非常特殊的定制需求否则没必要重复造轮子Redisson 在分布式锁这块的成熟度已经经过大量生产环境验证。1.3 方案选型的横向对比为了让你更清楚 Redisson 的定位我把常见的几种分布式锁方案拉出来对比一下。方案实现复杂度可重入自动续期阻塞唤醒性能适用场景数据库行锁低否否否差低频、强一致要求缓存 SETNX 手写中需自研否需自研好简单场景、能接受缺陷Redisson低框架封装是是是好绝大多数分布式并发场景协调服务类方案中是会话保活是中对一致性要求极高的场景从表里能看出来Redisson 在功能完整度和使用成本之间取得了很好的平衡。数据库行锁虽然简单但性能是硬伤高并发下数据库连接池很快耗尽协调服务类方案一致性更强但部署运维成本高而且性能不如缓存方案。Redisson 站在缓存中间件肩膀上既拿到了缓存的性能又通过框架补齐了功能短板。2. Redisson 锁的核心机制拆解2.1 可重入是怎么用 Hash 结构实现的理解 Redisson 锁的关键是先搞懂它存的数据结构。它没有用简单的字符串键值而是用了 Hash外层 key 是锁的名字比如myLockHash 里的 field 是客户端标识格式是UUID:线程IDvalue 是这个客户端对该锁的重入次数。为什么这么设计因为可重入的本质是同一个持有者再次加锁时不应该被自己挡住。用字符串结构的话你没法区分锁被自己占着和锁被别人占着只能一刀切地拒绝。换成 Hash 之后加锁时先判断 field 是否存在不存在说明没人持有写入 field 并设计数为 1存在且是自己的标识计数加一加锁成功存在但是别人的标识加锁失败。解锁逻辑对应地也要走 Lua 脚本判断 field 是否存在不存在直接返回存在则计数减一减完如果大于零说明还有外层锁没释放只更新计数不删键减完等于零才删除整个 key 并发布解锁消息。这一整套判断加操作必须原子完成所以 Redisson 全部用 Lua 脚本下发到服务端执行避免客户端多次往返带来的竞态。2.2 看门狗续期的时间账怎么算看门狗是 Redisson 锁最容易被误解的机制很多人以为加了锁就自动永久续期其实不是。它的工作逻辑是这样的加锁成功时如果没有显式指定过期时间默认lockWatchdogTimeout是 30 秒同时启动一个定时任务每隔30/3 10秒检查一次如果业务还没执行完锁还被当前线程持有就把过期时间重置回 30 秒。这里有个关键点只有不指定 leaseTime 的时候看门狗才生效。如果你调用的是lock.lock(10, TimeUnit.SECONDS)这种带过期时间参数的方法Redisson 认为你对自己的业务耗时心里有数不会启动看门狗10 秒一到锁就强制释放哪怕业务还在跑。这个设计经常坑到人——有人为了保险随手传了个过期时间结果业务执行超过这个时间锁提前释放并发问题照样出现。那默认 30 秒够不够对于绝大多数接口级业务足够了因为看门狗会持续续期理论上只要进程活着、线程没挂锁可以一直持有下去。真正需要担心的是进程崩溃的场景节点挂了看门狗自然停止锁最多 30 秒后自动释放这个兜底时间是可以接受的。如果你觉得 30 秒太长可以通过配置lockWatchdogTimeout调小但要注意续期间隔会跟着变成三分之一调太小会增加服务端压力。2.3 阻塞等待靠发布订阅而不是自旋拿不到锁的时候怎么办手写方案常见的做法是while循环不断重试也就是自旋。自旋的问题很明显CPU 空转浪费资源重试间隔不好把握间隔太短压力大间隔太长响应慢。Redisson 用的是发布订阅模型。线程加锁失败后不是立刻返回也不是死循环而是订阅这个锁的解锁频道然后阻塞挂起。等持有锁的线程释放锁时往频道里发一条消息所有订阅者被唤醒重新竞争加锁。这样既避免了 CPU 空转又能在锁释放的第一时间响应延迟很低。这个机制在lock()无参调用时默认生效等待时间不设上限直到拿到锁为止。如果你不想无限等可以用tryLock(waitTime, leaseTime, unit)超过 waitTime 还没拿到就返回 false业务自己决定是降级还是报错。我一般推荐用带超时的 tryLock因为无限制等待在极端情况下可能把线程池拖垮。3. 从零搭一套可运行的锁环境3.1 依赖引入与客户端配置先看依赖。Maven 项目里引入 Redisson 的 starter版本建议跟你的缓存中间件服务端版本对齐避免协议不兼容。dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.0/version /dependency配置文件里把服务端地址、密码、数据库编号填上。这里有个细节如果是集群模式配置项是clusterServersConfig单机是singleServerConfig别配错了。spring: redis: host: 127.0.0.1 port: 6379 password: your_password database: 0 timeout: 3000如果你不用 Spring Boot 的自动装配也可以手动构建客户端Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setPassword(your_password) .setDatabase(0) .setConnectionMinimumIdleSize(8) .setConnectionPoolSize(32); RedissonClient client Redisson.create(config);连接池参数值得说道说道。connectionPoolSize默认是 64connectionMinimumIdleSize默认 24如果你的锁竞争很激烈连接数不够会导致获取连接排队表现出来就是加锁变慢。我一般按并发线程数 × 1.5来估算池大小同时把最小空闲连接设成池大小的一半避免频繁创建销毁连接。3.2 加锁解锁的标准写法最基础的用法长这样Autowired private RedissonClient redissonClient; public void doBusiness(String orderId) { RLock lock redissonClient.getLock(lock:order: orderId); try { boolean acquired lock.tryLock(5, TimeUnit.SECONDS); if (!acquired) { throw new RuntimeException(获取锁失败稍后重试); } // 业务逻辑 processOrder(orderId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(加锁被中断, e); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }这段代码有几个地方必须注意。第一tryLock抛InterruptedException必须捕获并恢复中断标志不能吞掉。第二unlock一定要放在finally里否则业务抛异常锁就泄漏了。第三unlock前要判断isHeldByCurrentThread()因为如果加锁失败进了 finally直接 unlock 会抛IllegalMonitorStateException。锁的 key 命名也有讲究我习惯用lock:业务名:业务ID的格式冒号分层便于在客户端工具里按前缀排查也避免不同业务的锁名撞车。粒度上尽量细化到单个资源比如按订单 ID 加锁而不是按整个订单表加锁锁粒度越细并发度越高。3.3 不同锁类型的适用边界Redisson 提供的锁不止一种选错了类型比不用锁还麻烦。锁类型获取方式特点典型场景可重入锁 RLockgetLock同线程可重复加锁通用互斥公平锁getFairLock按请求顺序获取防饥饿、顺序敏感读写锁getReadWriteLock读读共享、读写互斥读多写少联锁 MultiLockgetMultiLock多把锁同时成功才算成功跨资源原子操作红锁 RedLockgetRedLock多节点多数派成功对一致性要求极高公平锁保证先到先得代价是性能略低因为要维护等待队列读写锁适合缓存刷新这类读多写少的场景读操作之间不互斥能大幅提升吞吐联锁用来解决同时锁多个资源的问题比如转账要同时锁两个账户用联锁能避免死锁。红锁争议比较大它试图在多个独立节点上多数派加锁来提升可靠性但实现复杂、性能损耗大我个人建议除非有非常明确的一致性诉求否则优先用单节点的可重入锁配合合理的过期时间。4. 实操中的坑与排查实录4.1 锁提前释放导致并发穿透这是最高频的问题现象是偶发性的数据错乱日志里看不出明显异常。根因通常是给lock()传了 leaseTime看门狗没生效业务执行超过 leaseTime 锁就没了。排查方法在加锁后打印当前时间业务结束后再打印一次算一下耗时跟 leaseTime 对比。如果耗时接近或超过 leaseTime基本可以确诊。解决方式有两种要么去掉 leaseTime 让看门狗接管要么把 leaseTime 设成业务最大耗时的两到三倍并加上监控告警。提示不要用估算一个很大的 leaseTime来回避问题过大的过期时间在节点崩溃时会导致锁长时间无法释放影响面更大。让看门狗续期才是正解。4.2 unlock 抛 IllegalMonitorStateException这个异常的意思是当前线程没有持有这把锁却试图解锁。常见原因有三个加锁失败但代码还是走到了 unlock锁因为超时被自动释放了线程再去 unlock跨线程解锁A 线程加的锁 B 线程去解。对应的排查思路先确认 unlock 前有没有isHeldByCurrentThread()判断再检查业务耗时是否超过 leaseTime最后确认加锁和解锁是否在同一个线程里异步任务、线程池里很容易出现跨线程解锁。Redisson 的锁是线程绑定的谁加谁解这个约束必须遵守。4.3 看门狗不续期的几种情况有人反馈明明没传 leaseTime锁还是提前释放了看门狗没生效通常有这几个原因客户端实例被关闭了看门狗定时任务跟着停业务线程被中断续期任务也受影响用了tryLock(waitTime, unit)这种两参数重载它内部其实等价于 leaseTime 为 -1 走看门狗但如果用的是三参数版本传了具体时间就不续期。排查时可以在配置里把日志级别调到 DEBUG观察是否有续期相关的日志输出。另外要注意看门狗是客户端侧机制如果客户端所在节点网络抖动连不上服务端续期也会失败所以网络稳定性是前提。4.4 常见问题速查表现象可能原因排查动作解决方向数据偶发错乱锁提前释放对比业务耗时与 leaseTime去掉 leaseTime 或加大并监控解锁抛异常未持锁解锁/跨线程检查 isHeldByCurrentThread加判断、保证同线程加锁变慢连接池不足看连接池监控指标调大池大小与最小空闲锁一直不释放节点崩溃/未 unlock查客户端存活与 finally依赖过期兜底、补 finally等待线程堆积无超时等待看线程池活跃数改用带 waitTime 的 tryLock4.5 几个压箱底的经验第一个经验锁的粒度宁细勿粗。我见过有人用一把全局锁保护整个订单模块结果所有订单操作串行化吞吐直接腰斩。正确做法是按业务主键加锁不同订单互不影响。第二个经验加锁代码尽量短。锁内只放必须互斥的逻辑日志打印、远程调用、文件 IO 这些能挪出去就挪出去锁持有时间越短竞争越小看门狗续期的压力也越小。第三个经验给锁加监控。至少监控三个指标加锁成功率、平均等待时长、锁持有时间分布。加锁成功率骤降说明竞争加剧或者有死锁等待时长变长说明锁粒度太粗持有时间异常说明业务里有慢操作。这些指标接上告警很多问题能在用户感知之前发现。第四个经验测试环境模拟并发。用多线程或者压测工具模拟几十上百个并发请求打同一把锁观察是否只有一个线程进入临界区同时验证异常路径下锁能否正确释放。这个测试花不了多少时间但能提前暴露大部分配置问题。5. 高并发场景下的进阶调优5.1 锁分段提升吞吐当单个热点资源的并发量特别大时单把锁会成为瓶颈。这时候可以考虑锁分段把一个资源拆成 N 个槽位每个槽位一把锁请求按哈希落到不同槽位。比如库存扣减把 1000 件库存拆成 10 段每段 100 件请求先随机选段再扣减段内互斥段间并行吞吐能提升接近 N 倍。代价是逻辑复杂度上升要处理某段库存不足时跨段扣减的问题还要考虑分段不均导致的负载倾斜。所以锁分段适合明确的热点场景不是所有地方都值得上。5.2 读写锁优化读多写少缓存刷新、配置加载这类场景读操作远多于写操作用普通互斥锁会让读请求也串行化完全没必要。换成RReadWriteLock多个读锁可以同时持有只有写锁才独占读吞吐能提升一个量级。RReadWriteLock rwLock redissonClient.getReadWriteLock(lock:config); RLock readLock rwLock.readLock(); readLock.lock(); try { return loadConfig(); } finally { readLock.unlock(); }要注意读写锁的公平性问题默认是非公平的写锁可能被读锁饿死。如果写操作对延迟敏感可以用getFairLock的思路配置公平模式但会牺牲一部分性能。5.3 联锁规避多资源死锁转账场景要同时锁转出账户和转入账户如果两个线程分别锁了 A 和 B再互相等对方释放就死锁了。联锁的思路是把多把锁打包成一个原子操作要么全成功要么全失败从根上避免部分持有。RLock lockA redissonClient.getLock(lock:account:A); RLock lockB redissonClient.getLock(lock:account:B); RLock multiLock redissonClient.getMultiLock(lockA, lockB); boolean acquired multiLock.tryLock(3, TimeUnit.SECONDS);联锁内部会按固定顺序加锁保证不同线程的加锁顺序一致从而消除循环等待条件。这个细节很关键如果顺序不固定联锁也救不了你。5.4 参数调优的量化思路调参不能拍脑袋得有数据支撑。lockWatchdogTimeout的调整依据是业务 P99 耗时如果 P99 是 5 秒默认 30 秒的过期时间绰绰有余没必要动如果业务里有长任务动辄几分钟可以把过期时间调到业务最大耗时的 1.5 倍左右同时接受节点崩溃时锁释放变慢的代价。连接池大小的调整依据是并发加锁线程数池大小至少等于峰值并发线程数否则会出现等连接的情况。可以用connectionPoolSize 峰值QPS × 平均锁持有时间粗略估算再留 30% 余量。等待时间的设置依据是业务能容忍的延迟tryLock的 waitTime 不要超过接口超时时间否则等待还没结束请求已经超时了白白占用线程。我一般设成接口超时的 60% 到 80%。6. 写在最后的几句实在话分布式锁这东西用对了是并发控制的利器用错了就是埋在生产环境里的定时炸弹。Redisson 把大部分复杂度封装掉了但封装不等于免责锁的粒度、过期时间、异常路径、监控告警这些还是得自己把关。我个人的习惯是任何一把新加的分布式锁上线前必须过三关——并发测试验证互斥性、异常测试验证释放逻辑、监控面板验证可观测性三关都过了才敢放量。另外提醒一句分布式锁解决的是互斥问题不是一致性问题。如果你的业务对数据一致性要求极高光靠锁是不够的还得配合数据库事务、幂等设计、对账补偿这些手段一起上。锁只是并发控制链条上的一环别指望它包打天下。后续如果业务规模继续涨可以再往锁分段、读写分离、甚至本地锁加分布式锁的两级方案上演进但那是另一个话题了先把当前这把锁用稳再说。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。