
做了几年业务系统一定会遇到那种尴尬时刻接口要幂等、定时任务要防重复执行、库存要防超卖、状态机要防乱跳。单机时代锁住几行代码就完事可应用一旦多实例部署、微服务拆分synchronized和ReentrantLock立刻变成摆设——它们锁的是JVM进程内的线程换个节点就完全不认账。分布式锁就是为这种跨进程协调而生的。而在所有方案里Redisson 是对 Java 开发者最友好的一个它把分布式锁做成了 API 层面的开箱即用可重入锁、公平锁、读写锁、信号量、闭锁一应俱全底层还帮你处理了原子性和看门狗续期问题。这篇文章我会从单机锁失效讲起把 Redisson 的多样化锁实现、核心原理和实战选型完整拆一遍适合正在做分布式系统、准备分布式锁面试或者想弄清 Redis 锁到底怎么用才靠谱的读者。1. 先搞清楚单机锁为什么在分布式环境失效1.1 从一段经典业务冲突看锁的本质想象一个简单的库存扣减逻辑先查库存、再判断、最后减一。单体应用时代用synchronized包住这段代码JVM 会保证同一时刻只有一个线程进入临界区问题迎刃而解。但部署了两台应用节点后线程A在节点1查到的库存是10线程B在节点2查到的库存也是10两个节点同时执行减一最终数据库里库存变成了9而不是8超卖就出现了。这不是代码逻辑的锅而是锁的粒度放错了维度。synchronized和ReentrantLock是进程内锁它们依托的是 JVM 的监视器机制和内存屏障只对同一进程内的线程生效。分布式环境下多个进程各执一词必须引入一个所有节点都能访问的第三方协调者用它来统一裁决谁先进入、谁后等待。1.2 分布式协调者的分钟级方案业界常见的分布式锁实现核心思路都是在共享存储上登记占用状态。可以大致分为三大流派基于数据库用唯一索引或乐观锁版本号控制并发够简单但性能有限且数据库本身可能成为新的瓶颈基于 ZooKeeper / etcd利用临时顺序节点或租约机制强一致性有保障但引入额外组件运维成本高基于 Redis利用单线程模型下的原子命令性能极高、接入成本低但需要自己处理过期、重入、主从切换等细节。业内针对 Redis 分布式锁还有个专门的讨论是继续使用 Redis 的过期和 Lua 原子性还是引入一套完整的分布式协调系统。目前绝大多数互联网业务尤其是高并发、可接受极短时间不一致的场景都选择了 Redis 方案。Redisson 正是把这个方案做成了成熟框架的典型代表。2. Redisson 分布式锁的核心原理2.1 一次完整的加锁过程长什么样Redisson 的RLock用起来和 JDK 的 Lock 接口几乎一样lock()加锁、unlock()释放、tryLock(timeout)带超时等待。但这层 API 背后不是简单的SET key value NX EX而是一段精心设计的 Lua 脚本。加锁的核心逻辑大概是这样的流程先检查锁键是否存在不存在就创建 Hash 结构保存当前线程标识和重入次数然后设置过期时间如果锁键存在且线程标识相同就把重入次数加一否则返回剩余过期时间表示锁正在被别人持有。这整个判断加写操作封装在 Lua 脚本里由 Redis 保证原子执行。KEYS[1] 锁名称 ARGV[2] 客户端唯一标识(线程ID/UUID) ARGV[1] 过期时间毫秒数为什么非要 Lua因为如果先EXISTS判断再HSET这两步之间可能插入其他客户端的命令锁就可能被两个线程同时抢到。Redis 单线程模型下Lua 脚本可以保证多条命令的连续执行不被其他命令打断这是 Redisson 分布式锁安全性的根基。2.2 看门狗机制到底在守护什么分布式锁最常见的坑之一就是过期时间设置不合理。设短了业务还在执行锁就自动释放导致其他人也能进来设长了一旦客户端宕机锁会一直占着形成死锁。Redisson 的解决方案是默认过期时间 30 秒同时启动一个定时任务每 10 秒检查一次锁是否还被当前客户端持有如果持有就自动把过期时间重置为 30 秒。实际测试中30 秒足够覆盖绝大多数业务方法。 假设某个操作偶尔慢查询耗时 35 秒。 到期前 5 秒看门狗已续期锁并不会丢失。这个机制叫 Watch Dog它的价值是让锁的生命周期和业务执行时间自动匹配。你可以手动传leaseTime参数覆盖默认行为一旦手动指定看门狗就不会启动所有超时管理变成一次性设定。我的经验是除非对业务最大耗时非常有把握否则尽量别手动指定过期时间交给默认机制更稳妥。2.3 释放锁的细节比加锁更容易踩坑释放锁看似只是del一个键但直接删除会误删他人的锁。假设线程A持锁业务阻塞超过锁的过期时间锁自动释放线程B随后拿到同一把锁此时线程A终于执行完毕如果它毫无判断地del就会把线程B的锁删掉。Redisson 的解锁 Lua 脚本会先判断 Hash 中的线程标识是否一致一致才允许删除并返回成功否则直接返回空。这等于给锁加了一层身份校验从机制上杜绝了误删。同时它还会用信号量通知等待中的其他线程唤醒它们参与下一轮竞争。3. 多样化锁实现与实战选型3.1 可重入锁 RLock最常用的默认选项先说最常用的可重入锁RLock。锁名称一般对应一个业务资源比如「order:create:10086」。当你需要在一个方法里重复获取同一把锁时RLock 允许你嵌套加锁而不产生死锁内部通过计数器实现每次加锁加一每次解锁减一减到零才真正释放。实际项目里最典型的使用场景是防重复操作。比如用户连续点击两次生成订单或者两台服务器同时执行同一个定时任务。用 RLock 包住关键代码同一个锁名称下只有一个客户端能进入。我在项目中习惯把锁的名称设计成「业务前缀:资源类型:资源ID」这样既方便排查也能精细控制锁的粒度。RLock lock redissonClient.getLock(pay:order:10001); if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { try { // 执行业务 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }3.2 公平锁 RFairLock先来先得的强排队语义默认的可重入锁是非公平的也就是说即使线程A先等待线程B后到也可能插队成功。绝大多数业务不在乎这一点但某些场景要求严格的先到先得比如按照请求顺序分配资源、生成排队号。RFairLock 通过 Redis 的 ZSet 和 List 结构把等待中的客户端组织成有序队列谁先请求谁先拿到锁。代价是公平锁的性能比非公平锁差每次加解锁都要维护队列顺序。我只有在真正需要顺序履约的业务里才使用它其他场景一律使用默认锁。3.3 读写锁 RReadWriteLock区分读多写少的高并发场景Redisson 提供的读写锁RReadWriteLock使用同一把 Redis 锁键内部通过 Hash 的 mode 字段区分读模式与写模式当写锁持有期间所有读锁和写锁请求都会阻塞当只有读锁持有期间多个读锁共享资源写锁必须等待。它的价值在于提升读多写少场景的并发度。比如配置数据大部分时间是被读取偶尔更新。如果都用可重入锁读操作之间也会互相排队换成读写锁后所有读请求可以并行只在写操作来临时短暂串行。唯一要提醒的是读锁是可重入的写锁也是可重入的但一个线程在持有写锁时再获取读锁是允许的反之则可能产生死锁风险注意锁顺序。3.4 信号量 RSemaphore用计数器做限流与资源池信号量不是锁的概念而是一个计数器。Redisson 的RSemaphore支持在 Redis 上初始化一个许可数量每次acquire()数量减一release()数量加一减到零时后续 acquire 会阻塞等待。这个特性特别适合做接口限流、连接池控制、共享资源的有界分配。我在网关层用它做过简单的并发控制初始化 200 个许可请求进来时先 acquire 再执行业务执行完 release超过 200 并发的请求会阻塞排队。相比 Resisl 的INCR过期窗口限流信号量更贴近有界资源的语义。RSemaphore semaphore redissonClient.getSemaphore(gateway:limit); boolean success semaphore.tryAcquire(1, 2, TimeUnit.SECONDS); if (success) { try { // 处理请求 } finally { semaphore.release(); } }3.5 闭锁 RCountDownLatch等待多节点完成任务如果你需要在所有节点都完成某个阶段后再统一继续Redisson 还提供了分布式倒计时锁存器的实现。初始化一个计数每次countDown()减一调用await()的客户端会一直等待计数归零。我把它应用在分布式批处理的场景里主任务拆分给多个 worker 节点并行处理每个节点完成后 countDown主任务汇总后继续后续步骤。比起自己写轮询或者消息通知闭锁的语义更直观缺点是计数只能单次使用不能重置复用。3.6 联锁 MultiLock 与红锁 RedLock跨节点加锁的安全加强方案普通 RLock 只锁一个 Redis 实例一旦该实例发生主从切换锁的持久化可能出现问题主节点故障后未同步的数据丢失从节点顶替后锁信息没了其他客户端就能趁虚而入。Redisson 为此提供了 MultiLock将多个独立锁合并成一组同时加锁只有全部成功才算加锁成功还存在一种红锁方案要求一半以上 Redis 节点成功。不过实际生产中红锁策略争议较大多数团队会优先选择强化可用性而非强化安全性或者干脆引入 ZooKeeper 保证强一致。我的建议是如果对锁的绝对安全有极高要求且能接受引入协调组件优先考虑 ZooKeeper如果已经大量使用 Redis且业务可以接受极短时间的锁失效窗口普通 RLock 配合看门狗已经是绝大多数公司的做法。4. 常见问题与排查技巧实录4.1 锁不生效的几种典型场景分布式锁最常见的问题其实是锁根本就没被正确使用。我遇到过最多的三种情况锁名称设计错误不同业务用了相同的锁名称导致不同请求互相排队或者同一业务在不同模块用了不同名称锁形同虚设加锁和解锁不是同一个客户端比如 A 节点加锁最终却在 B 节点的 finally 里调用 unlock虽然 Redisson 会校验线程标识但这种跨节点调用往往导致锁提前释放或抛异常忘记释放锁lock()之后没有放在 finally 里 unlock一旦业务异常退出锁只能等看门狗续期到进程结束其他人长时间阻塞。排查这类问题第一件事就是去 Redis 里看锁的存活状态和持有者标识再对照代码检查加锁、解锁的作用域和锁名称。4.2 持锁时间过长与性能瓶颈消费者经常觉得锁变慢了但往往不是因为 Redis 本身而是业务临界区太大。锁的粒度应该是保护资源状态变更的最小代码范围而不是把整个方法都包进去。我曾见过同事把整个服务调用链塞进锁里结果单机并发不到 100 就全阻塞了。性能调优的顺序应该是先缩小锁范围再考虑锁模式分级最后看是否能用乐观锁替代悲观锁。Redisson 锁本身在锁空闲状态下的开销很小关键瓶颈永远是业务代码在锁里做的事太多。4.3 监控和可观测性怎么搭Redisson 的锁是否正常工作不能靠猜。可以在请求进入锁前后打印耗时和锁状态对线程名附带一个锁标识更专业一点利用 Micrometer 暴露指标把锁等待时间、锁持有时间、获取锁失败次数发到监控面板。我还习惯在 redis-cli 里定期检查锁键数量如果某个锁名称长期存在且没有被释放说明代码里一定存在泄漏路径。4.4 几个保险丝级别的建议最后分享几点我踩坑总结出的经验提示一unlock()方法只能由持有锁的线程调用。在 finally 块里使用isHeldByCurrentThread()判断当前线程是否还持有该锁避免二次解锁抛出异常。这也是代码评审时我最常关注的点。提示二看门狗是 Redisson 的重要能力但手动指定leaseTime之后看门狗不会启动。如果业务中有长时间运行的任务手动指定过期时间要预留充足余量否则只能在解锁时报错后重新设计。提示三尽量不要让锁的键名太长或包含无关随机信息。锁名称等于资源标识推荐「domain:resource:id」格式便于在 Redis 里快速筛选和定位。提示四在微服务网关或定时任务框架里分布式锁常和幂等表搭配使用。锁负责短期互斥幂等表负责长期唯一两者协同才能真正免疫重复调用。提示五如果你正在准备分布式锁相关的技术面试光答Redisson 怎么用是不够的重点能说清 Lua 脚本的原子性、看门狗续期的原理、可重入计数的实现以及主从切换场景下锁可能丢失的边界问题。我在实际项目里的体会是分布式锁永远不是银弹它只是把资源竞争从一个进程推到了分布式环境。Redisson 的多样化锁实现让我们不需要反复造轮子但架构师的责任是用对锁的类型、设对锁的粒度、想清锁失效的边界。块锁、公平锁、读写锁、信号量、闭锁、联锁每种锁背后都有明确的使用语义和代价。理解透这些你才能真正驾驭高并发场景下那把看不见的锁。