搞分布式系统的人基本都绕不过分布式锁这道坎。无论是扣库存、防止重复下单、还是定时任务避免多节点重复执行你都需要一把跨进程的锁。刚开始做的时候大家习惯用SET NX EX自己撸一个看起来挺简单但真跑到线上各种奇奇怪怪的问题就冒出来了锁超时自动释放导致业务还没执行完锁就没了、A线程的锁被B线程释放掉、主从切换瞬间丢锁……这些问题排查起来一个比一个难受。后面我用上了Redisson这个库封装的分布式锁用起来就像操作JDK自带的ReentrantLock一样自然而且它内部把上面说的那些坑都填得差不多了。但有一说一网上关于Redisson锁的原理文章不少大部分却只停留在看门狗续期这个点上讲得太浅。你真正遇到锁失效、死锁、性能瓶颈的时候光知道一个看门狗是不够的。这篇我把Redisson分布式锁的核心原理从源码层面拆开揉碎讲清楚同时把我在实际项目中踩过的坑和处理思路一并写出来希望能帮到正在研究分布式锁的各位。1. 分布式锁的选型决策Redisson为什么会成为主流选择1.1 什么时候你真的需要一把分布式锁先聊聊分布式锁的使用场景别一上来就谈技术选型。很多团队的分布式锁是被需求推着走的真正要派上用场的场景大致有这么几类第一类是并发资源控制典型的就是电商库存扣减。库存数据一般放在Redis里多个服务实例同时对同一个商品SKU执行读取-扣减-写回如果不加锁超卖几乎是必然的。本地Synchronized或JUC Lock只能管住当前JVM里的线程多实例部署之后必须用跨JVM的锁。第二类是幂等性保障比如支付回调、MQ消费。消息中间件有至少一次的投递语义消费端如果没做幂等同样的订单可能被处理两次。分布式锁在这里的作用是确保同一个业务标识比如订单号在同一时间只被一个节点处理。第三类是定时任务防重。当你把定时任务部署到多台机器上的时候很自然地希望同一时刻只有一台机器在执行。这个问题用分布式锁解决非常顺手抢锁成功者执行。第四类是分布式事务中的资源协调比如多个服务需要按顺序操作同一个共享资源靠分布式锁编排顺序。这些场景的共同点是跨进程 临界区共享。判断自己是否需要分布式锁就看这两条是否同时成立。如果只有一个实例别引入分布式锁zookeeper、etcd、redis都别急着上本地锁最香省掉一大堆网络开销和运维成本。1.2 自己用SETNX实现锁坑在哪里很多教程会让你先用SET resource_key request_id NX EX 10自己实现一个说这样能理解原理。这个方向本身没错但如果你真的照这个方案上生产会接连踩到几个坑。坑一业务执行时间超过锁的过期时间。你给锁设置了10秒过期但业务卡了15秒锁提前释放了另一个线程就能拿到锁临界区瞬间失去保护。解决思路是续期但自己写续期逻辑就要额外起一个守护线程代码复杂度一下就上去了。坑二锁误删。A线程拿到锁执行中因为Full GC停顿了很长时间锁超时释放。B线程立刻拿到锁开始执行。A线程恢复后执行完自信地DEL掉锁——实际上删的是B线程的锁。虽然可以通过request_idToken来判断归属但判断和删除是两个操作必须用Lua脚本包成原子的才行很多人根本没想到这一层。坑三主从架构下的锁失效。Redis主从复制是异步的A线程在主节点写入锁key主节点还没来得及同步到从节点就发生故障切换从节点升为主节点后发现根本没有这个锁keyB线程趁虚而入。这是Redis分布式锁的先天缺陷每个方案都绕不开必须从更高的架构层面去权衡。坑四没有可重入能力。同一个线程进入两次加锁逻辑比如外层方法加了锁内部调用的方法又加了锁如果是非可重入锁直接就死锁了。自己实现可重入就需要记录持有者信息和重入计数又是一个不小的工程。这些坑理论上都能填但填完之后你相当于重新造了一个轮子而且大概率没有Redisson在极端场景下打磨得那么细致。这也是我选择分析Redisson的原因——它的核心实现思路本质上就是上面四个坑的标准解法。2. Redisson锁的存储结构与加锁流程全解析2.1 锁在Redis里到底长什么样Redisson加锁后Redis里并不是简单存一个字符串key1而是使用Hash结构这是它实现可重入的关键设计。我经常让团队里刚接触Redisson的同学做一件事手动在Redis客户端模拟一次加锁用HGETALL命令看看锁的结构。一次加锁之后你会看到一个类似下面的结构1) myLock 2) 1) 8b1e6f3d-5d77-4d9e-9c3e-2d4d2e6d8f12:thread-1 2) 1这个Hash的key是锁名称就是getLock(myLock)里传入的名称Hash里的field是UUID线程ID的组合value是重入计数。为什么field要用UUID:threadId这种格式两个原因。第一个原因是唯一标识持有者UUID区分不同JVM实例注意UUID在Redisson客户端启动时就生成一次线程ID区分当前JVM内的不同线程二者组合才能全局唯一地确定谁持有了这把锁。第二个原因是为可重入做准备后面重入一次就对value加一释放一次就减一减到0再从Redis里删除整个key。你可能会问为什么不直接用Redis的SETNX而要费劲用Hash因为SETNX在语义上就是个不存在则设置的命令它天然不支持计数。Hash配合HINCRBY才既能实现互斥又能记录重入次数。数据结构的选型从根上决定了锁的功能边界Redisson用Hash不是炫技是真的有需要。2.2 加锁的Lua脚本原子性是怎么保证的Redisson加锁的核心是一段Lua脚本。我看过的很多源码解析文章会把脚本直接贴出来然后说这段脚本就是加锁但很少讲清楚每一行为什么要这么写。我们先看核心部分然后逐行拆解-- KEYS[1]: 锁的名称如 myLock -- ARGV[1]: 锁的自动过期时间毫秒默认30000 -- ARGV[2]: 持有者标识UUID:threadId -- ARGV[3]: 随机签名用于防止锁误删一般是当前线程的随机token if (redis.call(exists, KEYS[1]) 0) then redis.call(hincrby, KEYS[1], ARGV[2], 1) redis.call(pexpire, KEYS[1], ARGV[1]) return nil end if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then redis.call(hincrby, KEYS[1], ARGV[2], 1) redis.call(pexpire, KEYS[1], ARGV[1]) return nil end return redis.call(pttl, KEYS[1])这段脚本的逻辑分成三个分支分支一锁不存在直接抢占。exists判断锁key不存在说明当前没有其他线程持有锁当前线程通过HINCRBY把重入计数设为1同时用PEXPIRE设置过期时间。注意PEXPIRE的单位是毫秒默认值30000毫秒也就是30秒后面会讲到这个30秒是看门狗续期的时间基准。分支二锁存在且持有者是自己重入。HEXISTS检查Hash中是否存在当前线程的UUID:threadId这个field。存在就把value递增1同时刷新过期时间。这就是可重入能力的底层实现同一个线程嵌套加锁不会阻塞只会让计数累加。分支三锁被其他线程持有返回剩余生存时间。这是最容易被忽视的一条路径。当锁被别人持有时PTTL返回当前锁的剩余过期毫秒数这个返回值会用于后续的阻塞等待逻辑——线程需要知道我还得等多久才能在合适的时候重新申请加锁。这里我要特地强调一下这段脚本之所以用Lua是因为Redis从2.6开始支持在服务器端原子地执行Lua脚本整个脚本作为一个整体执行执行过程中不会有其他命令插入。如果你不用Lua改成先在客户端判断、再执行HINCRBY判断和写入之间就有时间窗口并发条件下就可能让两个线程同时认为自己抢锁成功。这个原子性是分布式锁正确性的根基。2.3 阻塞等待与唤醒Redisson如何处理竞争我把Redis当成锁服务器它只负责保存锁状态和执行Lua脚本真正的线程阻塞和唤醒是在客户端完成的。当一个线程尝试加锁而锁正被其他线程持有时Redisson并不会让这个线程傻乎乎地死循环轮询Redis高频轮询会把Redis打爆。它的做法是订阅锁对应的Redis Channel然后让当前线程阻塞等待直到锁被释放再被唤醒重新尝试加锁。具体来说Redisson加锁失败时会进入一个while (true)循环循环内先subscribe锁的发布订阅频道然后Semaphore阻塞等待。当持有锁的线程解锁时会向这个频道发布一条解锁消息等待的线程通过监听器收到消息后立刻重新发起加锁请求。这个机制有两点值得玩味第一它是事件驱动而不是轮询驱动。锁释放后等待线程能立刻感知并尝试竞争延迟极低。不像手动SETNX sleep 重试那种方案线程醒了也不知道锁是否已经释放只能碰运气。第二它仍然保留了自旋重试的兜底逻辑。万一发布订阅消息丢失比如网络抖动等待线程也不会永久沉睡。Redisson在阻塞等待时设置了超时时间超时后会主动重试加锁。如果你对Redisson的锁做过压测会发现高竞争场景下它的表现依旧平稳靠的就是事件通知为主超时自旋兜底这套组合逻辑。2.4 加锁流程的时序总结我把加锁流程整理成一条逻辑线方便面试或者写文档的时候直接引用线程发起lock()调用Redisson生成全局唯一的UUID:threadId作为持锁标识。执行Lua脚本尝试加锁脚本内部通过exists和hexists判断是「抢占」还是「重入」。加锁成功启动看门狗WatchDog后台续期线程——这一步lock()和tryLock()有区别后面细说。加锁失败订阅锁释放的ChannelSemaphore阻塞当前线程。锁被释放收到订阅通知唤醒线程回到第2步重新尝试或者自旋等待超时后重试。3. 解锁流程、看门狗机制与可重入的底层实现3.1 解锁为什么也必须用Lua脚本解锁这段Redisson同样使用Lua脚本。核心脚本如下-- KEYS[1]: 锁名称 -- KEYS[2]: 锁的发布订阅Channel名称 -- ARGV[1]: 解锁的发布消息内容 -- ARGV[2]: 持有者标识 UUID:threadId -- ARGV[3]: 锁的默认过期时间 if (redis.call(hexists, KEYS[1], ARGV[2]) 0) then return nil end local counter redis.call(hincrby, KEYS[1], ARGV[2], -1) if (counter 0) then redis.call(pexpire, KEYS[1], ARGV[3]) return 0 else redis.call(del, KEYS[1]) redis.call(publish, KEYS[2], ARGV[1]) return 1 end这段脚本的逻辑同样分成三条路径我来拆解一下路径一锁不在自己手里直接返回。HEXISTS判断当前线程是否持有锁如果field不存在说明锁根本不是自己的直接返回nil什么都不做。这一步就杜绝了误删别人锁的问题。注意这里的return nil在Redis返回给客户端时会表现为nil但正常流程里你很难触发这条路径因为Redisson的每一次解锁都会先检查持有者只有自己持锁才会继续执行。它存在的意义更多是防止非持锁线程恶意解锁——这看起来多余却是分布式锁必须具备的安全边界。路径二重入计数不为零只减计数不删锁。HINCRBY将value减1如果减完后计数仍大于0说明这个线程还没完全退出临界区外层锁还没释放此时只需要刷新过期时间锁继续保留。这就是可重入锁的完整释放语义加锁几次就得解锁几次计数归零才真正释放。路径三计数归零删除锁并通知等待线程。减完后计数等于0毁灭说明是最后一次解锁此时用DEL删除整个Hash key同时通过PUBLISH向锁的Channel发送一条解锁消息。这条消息就是所有阻塞等待线程的起床哨它们收到后立刻发起新一轮的加锁竞争。每次看这段脚本我都要感叹一句先判断再删除这两个动作必须在一个Lua脚本里原子地执行。如果你把它拆成两条Redis命令比如先HEXISTS检查再DEL删除那在两条命令之间锁的持有者可能已经变了你就又踩回删除别人锁的那个坑了。Redisson把所有关键操作都封装成Lua脚本就是为了从根本上杜绝这种检查与执行分离导致的竞态。3.2 看门狗机制锁续期背后藏着的权衡看门狗WatchDog是Redisson分布式锁被讨论最多的机制网上的讲解很多但我发现很多人对其中的设计意图理解是错位的。我先说结论看门狗解决的核心问题是业务还没执行完锁先过期了这种超时错杀现象。Redisson加锁默认的过期时间是30秒。如果业务执行时间超过30秒锁就会提前释放另一个线程就能拿到锁临界区的保护就失效了。看门狗的续期机制是加锁成功后会启动一个后台线程每10秒执行一次每次执行都会检查锁是否还存在如果存在就把过期时间重置为30秒。这样只要线程还活着锁就一直在直到线程主动解锁看门狗才会停掉。这段逻辑可以用一张时序来理解但我更想强调一个大家容易忽略的点看门狗续期不是固定地每10秒续期一次而是在每次执行时重新设置新的过期时间。所以30秒、10秒这两个数字之间的关系是30秒是过期基准10秒是续期间隔只要续期线程在锁过期前执行到锁的生命周期就能不断延续。这里有一个很有意思的细节tryLock(long waitTime, long leaseTime, TimeUnit unit)这个方法如果你手动传入了leaseTime自定义过期时间看门狗是不会启动的。这个设计很多人第一次看到会觉得反直觉为什么我传了过期时间反而不给我续期了原因在于语义约定你手动设置过期时间就代表你明确知道自己业务最长执行多久你主动告诉Redisson超过这个时间你就把锁释放吧。如果你传了leaseTime还让看门狗继续续期这两个参数就会相互冲突——你一边说10秒后释放一边又让后台线程把过期时间刷新回30秒整个逻辑就撕裂了。所以使用Redisson有一个很反直觉的实践要点只要你需要看门狗的自动续期能力就不要传leaseTime参数让锁使用默认的30秒看门狗续期模式。只有当你确定业务执行时间可控、且你完全不希望锁超长待机时才手动设置leaseTime。我在实际项目里见过不止一次同事为了让锁更可靠特意传了一个很长的leaseTime结果看门狗没启动业务卡顿后锁照样提前释放瞬间产生并发问题。3.3 可重入的完整生命周期借用JUC的ReentrantLock来理解Redisson的可重入会非常顺畅。我把它们的对应关系列一下概念ReentrantLockRedisson锁标识当前线程对象UUID:threadId字符串状态存储JVM内存中的state字段Redis Hash的value计数加锁compareAndSetState(0→1)Lua脚本HINCRBY解锁状态减1直到0Lua脚本HINCRBY -1直到0再DEL重入state累加Hash value累加JUC和Redisson在这个问题上的思路惊人地一致都是持有者标识 计数器的模式。区别只是存储载体不同——一个在JVM内部一个在Redis里。如果你已经理解了ReentrantLock再看Redisson的可重入其实就是把内存换成了Redis把CAS换成了Lua脚本。实际开发中有个典型的可重入场景你在Service层的A方法里加了锁在A方法内部又调用了B方法B方法也用同一把锁做了加锁。如果是非可重入锁B方法必然死锁有了可重入能力B方法会直接重入成功计数器从1变成2。注意这种情况下你必须保证解锁次数与加锁次数一致A解锁一次、B解锁一次。Redisson的RLock实现lock()和unlock()必须成对出现如果你在A方法里调用了lock()但在B方法里只lock()不unlock()计数器就永远归不了零锁会被一直持有直到看门狗误伤——实际上看门狗会一直续期相当于你的锁变成了永久锁这是很危险的。3.4 unlock到底能不能删别人的锁我在排查问题的过程中见过一次非常典型的误用用一个tryLock()返回了false的线程去执行unlock()结果那把锁被解掉了。我当时第一反应是源码里不是有HEXISTS判断吗怎么会解掉后来才发现问题出在调用者自己身上。Redisson的unlock()内部确实会检查持有者如果你不是锁的持有者它会直接返回根本不会碰别人的锁。但有一个前提你必须在加锁成功后才调用unlock。如果你在tryLock()返回false压根没拿到锁的情况下代码里还是调用了unlock()Redisson会因为当前线程未持有锁而抛出IllegalMonitorStateException而不是静默地删除那把锁。这个设计我要给个好评。抛出异常比静默不做事要好得多它能立刻暴露出调用者的逻辑错误。但很多团队的代码是这么写的if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // do something } else { log.warn(获取锁失败); return; } lock.unlock();看到了吗unlock()被放在了if外面不管有没有拿到锁都会执行。如果tryLock()返回false这个unlock()就会抛异常而且是在你正常返回之后抛异常信息还不容易跟业务请求关联上。正确写法应该是把unlock()放在finally块里并且确认加锁成功后才进业务代码。这一点老生常谈但我真在公司里的核心链路上见过不下三次。4. 源码视角RLock接口、公平锁与红锁争议4.1 RLock与RedissonLock的工程结构看Redisson源码时建议先看清楚接口体系再从入口往下追。Redisson的分布式锁顶层接口是RLock它继承了java.util.concurrent.locks.Lock接口同时补了tryLock(long waitTime, long leaseTime, TimeUnit unit)这类支持自动解锁的扩展方法。RedissonLock是它的核心实现类里面承载了前面分析的所有Lua脚本逻辑。从工程角度看RedissonLock内部有几个关键部件CommandAsyncExecutor所有Redis命令和Lua脚本的异步执行器Redisson用Netty的异步模型来处理命令交互。这也是Redisson性能好的重要原因——它把大部分需要等待的Redis操作都异步化了阻塞等待都是在业务侧用Semaphore模拟出来的。PubSub用来订阅锁Channel的监听器容器负责收到解锁消息后唤醒等待线程。LockResetter看门狗续期线程的调度器基于Netty的HashedWheelTimer时间轮实现定时任务。很多人会觉得分布式锁嘛核心就看个脚本就完了其实不然。工程性能上的差距往往就藏在异步执行器、时间轮调度这些看似不起眼的地方。同样是加锁用同步命令与用异步命令组合锁竞争激烈时的吞吐表现能差出好几倍。4.2 等待锁时线程在做什么前面提到过加锁失败后线程会进入订阅 阻塞等待的模式。很多教程到这里就结束了但我特别喜欢深挖一层这个阻塞等待到底是怎么实现的它是不是真的让JVM线程挂起看RedissonLock的源码等待逻辑大致是这样加锁失败后调用subscribeFuture去订阅锁的Channel然后getEntry().getLatch().tryAcquire(leaseTime, TimeUnit.MILLISECONDS)这个Latch本质是一个Semaphore初始许可数为0。线程在tryAcquire上等待只有两种唤醒路径订阅监听器收到解锁消息调用latch.release()释放一个许可线程被唤醒。等待超时比如tryLock传了最大等待时间线程被唤醒并返回false。理解了这个机制你就能明白一个常见疑问为什么Redisson的等待线程在锁竞争激烈时CPU占用率并没有飙升因为线程确实被挂起了它不是自旋轮询。这是事件驱动和轮询在资源占用上天然的本质区别。当然如果你的条件允许Redisson还提供了RedissonSpinLock——完全自旋的锁实现适用于临界区极短、等待线程不多的高吞吐场景。用哪个看你的业务模型没有银弹。4.3 公平锁与默认锁的差异Redisson还有个容易被忽略的功能getFairLock()公平锁。它和普通锁的关键区别在于等待的线程要按先来后到的顺序获取锁而不是靠谁手快谁先抢。公平锁的内部实现是维护了一个FIFO队列加锁时先用一个Lua脚本检查队列头部是否有等待者如果有当前线程只能排队。源码里公平锁的脚本比普通锁复杂得多因为它要处理排队序号队列清理唤醒下一个这些逻辑。我的实际建议是除非业务有强制的公平性要求否则用默认的非公平锁就够了。为什么因为公平锁的吞吐量通常更低。Redis执行Lua脚本本身就有开销公平锁一个加锁动作要执行比普通锁复杂得多的脚本QPS能差出一个量级。分布式锁解决的是互斥问题不是公平问题后者只有在极少数业务里才是硬需求。清楚自己到底要什么比盲目选择更高级更可靠的方案重要得多。4.4 关于RedLock业界吵了很久的话题聊到Redis分布式锁绕不开RedLock。RedLock本质上是一个由Redis作者提出的多节点锁算法建议在多个独立的Redis节点上同时加锁超过半数加锁成功才认为加锁成功目的是解决主从切换时的锁丢失问题。这个算法当年被Martin Kleppmann公开质疑过反方阵营也给出过回应两边都有道理。我对RedLock的立场一直是谨慎观望。首先大部分业务团队没有五个独立Redis节点的资源占比超半数、同步协调的成本也不低。其次RedLock无法解决暂停导致锁过期后GC恢复继续执行这种客户端侧的问题它只是把单点问题做了一定程度的缓解并非银弹。如果你真的对锁的可靠性有超乎寻常的要求比如金融级对账、资金操作与其引入RedLock不如认真考虑ZooKeeper或etcd实现的分布式锁——它们基于顺序节点的机制在锁状态一致性故障感知上比Redis锁要强不少。这并不影响Redisson作为Redis锁方案的主流地位。对大多数互联网业务的常规并发场景Redisson锁已经做得足够好。把分布式锁为什么需要超过半数节点这种问题留给面试官就好做工程的人更关心的应该是锁在你的业务场景下会不会失效、失效能造成多大损失、能否通过其他手段兜底。5. 实战向Redisson锁的使用细则、常见故障与排查记录5.1 结合业务的锁设计锁粒度与被保护资源的匹配很多人用分布式锁随手RLock lock redissonClient.getLock(order_ orderId);就完事但锁粒度设计不对也是隐性事故的温床。锁粒度要跟被保护的资源边界严格对齐。比如扣减库存如果多规格商品的库存是分开的那把productId:specId拼成锁key是合理的但如果把所有规格都用一个productId维度加锁那么不同规格的购买请求之间会相互阻塞吞吐量白白损耗。反过来也是如果本该一把锁保护的两个操作拆成了两把锁并发条件下就会同时进入临界区数据一致性瞬间破功。我推荐一个简单粗暴的判断标准你在加锁的临界区里操作的数据它的唯一标识范围就是锁的最小粒度。你对Redis Key inventory:{skuId}做读写那锁的范围就应该是这个skuId不需要向上聚合也不要向下拆分。还有一点容易被忽略锁的key命名空间要全局统一。一个系统里可能有多套业务共用同一个Redis Cluster如果只拿userId当锁key不同业务模块之间就会产生锁冲突。正确做法是在锁key中带上业务前缀比如order:pay:lock:{orderId}inventory:lock:{skuId}。这个习惯在微服务拆分之后尤为重要因为不同服务的Redis往往是共享的。5.2 常见故障一线程持有锁时间过长导致任务堆积这是我在性能排查中遇到过最多的问题。现象是某个用Redisson分布式锁保护的定时任务业务处理变慢之后大量请求堵在tryLock的等待上表现为接口TP99急剧上升Redis的slowlog里出现大量Lua脚本超时。排查下来原因通常有两个一是临界区里做了重活比如远程调用、批量DB更新二是锁等待时间设置得太大线程们长时间挂在锁上不放。对应的处理思路有二缩小临界区。分布式锁只保护真正需要互斥的部分比如检查当前处理状态和写入处理中标记那些不涉及共享资源的耗时操作移出临界区。锁里的代码越短线程占锁时间越短系统整体吞吐就越高。合理设置等待时间。tryLock(waitTime, leaseTime, TimeUnit)里的waitTime并不是越大越好你需要的只是能容忍的最大排队时长超过这个时长直接放弃并返回失败比无限等待更符合生产要求。比如一个接口的正常执行时间是50ms你把waitTime设成30秒每个线程平均等15秒这不叫容错这叫拖垮系统。5.3 常见故障二锁未释放导致死锁还有一类故障很隐蔽线程执行到一半抛了异常unlock()没有执行到锁一直不释放后续请求全部卡死。这种情况往往是代码没有把解锁放进finally导致的。我用Redis客户端HGETALL一看锁的Hash躺在那里持锁者还是那个已经消失的线程计数不为0看门狗还在持续续期好嘛真的锁死了。处理这种问题的正确姿势一是代码上保证try { ... } finally { lock.unlock(); }是铁律二是在监控层面做兜底——比如在Redis侧扫描长时间未释放的锁key或者在业务入口记录加锁与解锁的日志配对发现前置有锁、后置无解锁日志的情况立刻告警。关于锁异常分布还有个高频点unlock()本身也可能抛异常。比如线程在finally里调用unlock()时因为之前加锁已经超时被Redisson主动释放当你传入leaseTime时此时unlock()会抛IllegalMonitorStateException。所以unlock()的执行必须放在finally的最内层且要严格保证与lock()成功配对。5.4 常见故障三主从切换瞬间锁丢失这个故障和RedLock争议是一脉相承的。当你的Redis采用主从架构时如果主节点刚写入锁key还没来得及同步到从节点就宕机新的主节点上没有锁的痕迹另一个线程就能乘虚而入。Redisson默认方案解决不了这个问题Redis官方给的答案是RedLock但它要求你部署多个独立的Redis实例很多团队根本没这个条件。我的建议是分场景看待业务能接受极小概率的锁失效吗比如活动秒杀的库存扣减偶尔超卖一单问题不大那用Redisson默认方案就够了。如果业务绝对不能接受锁失效比如资金类的幂等处理那请把锁和唯一性约束叠加使用锁负责挡并发数据库的唯一索引或状态机负责兜底。我从来不建议把分布式锁当成银弹中的银弹它只是一个并发控制手段你的系统一定还有校验和兜底的层次这一点心里要有数。5.5 故障排查时的Redis命令速查遇到锁相关的线上问题我最常做的排查动作就是连上Redis用几个命令快速定位现场情况。这里整理一个速查表排查目的命令判断依据查看锁是否还存在EXISTS myLock返回1表示存在0表示已释放查看锁的持有者和重入次数HGETALL myLock能看到持锁线程标识和计数查看锁剩余过期时间PTTL myLock小于0说明已过期模拟持锁者的续期PEXPIRE myLock 30000临时延长锁时间给排查留出窗口检查Redis慢查询SLOWLOG GET 10查看是否有Lua脚本超时日常巡检时可以周期性地扫描几百个锁key看看有没有长尾未释放的。这不是给Redisson擦屁股而是分布式系统运维的常规动作——你永远无法保证所有代码路径都是优雅退出。Redis锁只是工具你得为它建立一套完整的观测手段。6. 从实践到面试把原理讲清楚比背结论重要Redisson分布式锁是面试高频题但我发现很多候选人的回答模式是Lua脚本 看门狗 可重入三板斧讲完就停。面试官稍微追问一下看门狗具体怎么续期的锁等待过程中线程在干什么立刻就卡壳了。这说明很多人的认知是知道结论没理解机制。一个好的回答路径应该是这样的先说自己对分布式锁本质的认知——锁就是一个跨进程的互斥变量难点在于争夺、释放、异常兜底然后引出Redisson用Hash Lua脚本解决互斥与可重入用看门狗解决超时续期用发布订阅解决等待唤醒最后必须落到这个方案有什么代价适合什么场景不适合什么场景。这样答出来的感觉才是真正做过工程的深度。从面试的角度我建议额外把tryLock和lock的区别、leaseTime与看门狗的关系、以及为什么Lua脚本能保证原子性这三个点准备透。这三个点是区分背过文档和读懂源码的分水岭。我自己带团队时有一个习惯凡是要引入中间件不管是缓存、锁还是消息队列都要让核心研发把最坏情况下它会怎么挂写进设计文档。Redisson分布式锁也一样——你心里必须清楚它默认依赖于Redis的可用性Redis一挂锁就不可用它默认租约续期依赖于看门狗线程进程一卡续期就可能滞后到线程卡拐点它默认只能保护作用于Redis保护的数据你用它去锁MySQL事务性操作是概念错配。想清楚了这些边界条件你才真正掌握了分布式锁。另外一个实用一点的经验Redisson锁使用中建议要给Redis锁操作配上独立连接池或至少独立的RedissonClient实例避免锁的获取/释放流量和普通业务缓存流量互相争抢连接造成不必要的命令排队。越是在高并发场景下一个小小的连接池隔离能帮你的锁操作减少大量等待时间。最后再说个排查技巧如果你怀疑锁的请求量太大导致Redis高负载除了优化业务你还可以在RedissonClient层面开启自定义监听器统计lock和unlock的耗时和调用频次通过监控曲线一眼看出锁的竞争激烈程度再决定是优化锁粒度还是引入本地锁前置过滤。我靠这个手段排查过不止一次线上偶发超时问题说实话比纯靠猜效率高太多了。