资讯动态

Redisson MultiLock分布式锁深度解析:跨节点强一致锁的原理与实战避坑

发布时间:2026/10/15 23:32:07 来源:尧图企业网站定制
分布式锁这块大家用得最多的就是 Redisson 的RLock。单个 Redis 节点上tryLock、unlock一套下来配合看门狗续期确实能解决大部分并发问题。但一旦牵扯到跨节点、跨实例的强一致场景单把锁就撑不住了。这时候 Redisson 提供的MultiLock就派上用场。这篇文章不绕弯子直接拆解 MultiLock 的原理、源码层面的加锁路径、实际使用中的坑以及它和 Redisson 普通锁的本质区别。先说个结论MultiLock 不是把多把锁各自加一遍那么简单它的核心语义是要么全部拿到要么一把都不拿。这个语义在分布式环境下做跨资源保护时非常关键。1. MultiLock 解决什么问题1.1 单节点分布式锁的局限性单把 Redisson 锁的流程大家都很熟向 Redis 写入一个带过期时间的 keykey 存在则加锁成功key 不存在则加锁失败通过 Lua 脚本保证检查和写入的原子性。这套机制在单节点 Redis 实例上工作的很好性能高、延迟低、实现简单。但单节点锁有一个天然的假设前提这个 Redis 节点必须是可靠的。一旦节点出问题整个锁体系就跟着出问题。比如这个节点发生了主从切换旧主节点上的锁 key 还没来得及同步到从节点新的主节点起来了此时另一个客户端就能对同一个资源加锁成功这就出现了两个客户端同时持有同一把锁的情况业务层面就会产生数据竞争。单节点锁的另一个问题是资源分布。假设你的业务涉及多个独立的 Redis 集群或者多个不同的存储系统你想在两个资源上都加锁保证操作的原子性。单把锁只能保护其中一个资源另一个资源仍然可能被其他线程直接操作。要同时保护多个资源就需要一把组合锁。1.2 主从架构下的锁失效场景很多团队把 Redis 部署成主从模式主节点负责写从节点负责读Redisson 默认也是连主节点。如果锁的 key 只写在主节点上主从之间通过异步复制同步数据。问题是异步这两个字意味着从节点上的数据可能落后于主节点。我在一个高并发交易系统里就遇到过这种问题。当时用的是单节点 Redisson 锁线上偶发出现两个线程同时执行同一个订单的退款操作。排查下来原因就是主 Redis 节点发生了故障切换一个线程在旧主节点加的锁随着旧主节点下线而消失另一个线程在新主节点上顺利加锁成功两个线程就同时进入了临界区。这个场景下普通的 Redisson 锁解决不了。你不可能依赖 Redis 的主从同步来保证锁的持久性因为同步始终是异步的存在时间窗口。想让分布式锁在故障切换场景下依然可靠要么引入类似 Redlock 的算法要么就是多个独立节点同时加锁降低所有节点同时出问题的概率这正是 MultiLock 的定位。1.3 MultiLock 的设计目标MultiLock 的设计目标很明确通过同时对多个独立的 Redis 节点或资源加锁把单点故障的风险摊出去。只要不是所有节点同时不可用锁就仍然成立。它不是替代 Redisson 单节点锁而是解决单节点锁在特定场景下的不足。如果你有多个资源需要一起保护或者你的 Redis 部署形态存在主从切换风险MultiLock 是一个比单锁更合理的选择。但如果你的场景只是单 Redis 实例上的普通互斥用 MultiLock 反而是多余的性能和代码复杂度都不划算。2. MultiLock 的核心设计思路2.1 从单锁到多锁的抽象Redisson 的 MultiLock 从 API 层面来看就是把多个RLock组合成一个RLock然后用统一的接口去操作这组锁。这里有一个很多人忽略的细节MultiLock 本身并不持有真正的锁状态它只是一个门面真正的锁状态还是分散在各个底层RLock对应的 Redis key 上。代码层面是这样的RLock lock1 redissonClient1.getLock(resource:order:1001); RLock lock2 redissonClient2.getLock(resource:order:1001); RLock multiLock redissonClient.getMultiLock(lock1, lock2);你可以给同一个 RedissonClient 创建多个锁也可以给不同的 RedissonClient 创建锁再组合。底层不管是同一个 Redis 还是多个 RedisgetMultiLock接收的是若干RLock对象Redis 实例不一定是同一个。这样设计的价值在于抽象一致。调用方不需要关心自己是加了单锁还是多锁统一走tryLock、unlock接口。但内部实现上MultiLock 和单锁的差异非常大后面我会详细拆解。2.2 加锁的成功条件全部持有MultiLock 加锁成功的条件非常严格必须先对列表里所有的锁依次加锁全部成功MultiLock 才算获取成功。这个全部意味着如果其中任何一把锁加锁失败之前已经加锁成功的锁会被立即释放然后整体返回失败。这个动作叫做回滚它是 MultiLock 保证要么全部拿到要么一把都不拿的关键。为什么必须回滚举个例子你有三个锁节点 A、B、C线程 T1 在 A 和 B 上加锁成功C 加锁失败。如果 T1 不及时释放 A 和 BT2 来了之后它试图加锁A 和 B 一直被占用T2 只能一直重试或者失败。就算最后 C 上的竞争结束了T2 成功拿到了 C但它拿不到 A 和 B。这样业务的进程就死锁了。回滚机制就是在失败时释放已经成功的锁避免锁资源被无意义占用。2.3 锁顺序与占用期的关系MultiLock 加锁时遍历ListRLock的顺序直接决定了锁资源的竞争效率。默认实现中MultiLock 按传入锁的顺序依次加锁。这个顺序在实际工程中非常关键如果多个线程以不同的锁顺序去加同一组锁就可能出现死锁。比如线程 A 先锁 lock1再锁 lock2线程 B 先锁 lock2再锁 lock1。A 拿到 lock1 后等待 lock2B 拿到 lock2 后等待 lock1两个线程互相等待直到超时。虽然 Redisson 在超时后会释放已持有的锁避免永久死锁但等待超时的时间成本是实打实的。因此使用 MultiLock 时所有请求必须保持一致的加锁顺序。我通常会在公共代码里封装一个固定顺序的锁工厂避免调用方自由排列顺序。3. 加锁与释放的核心实现3.1 MultiLock 加锁的调用链路与逐节点加锁MultiLock 的加锁链路看起来非常直接tryLock内部遍历所有锁逐个调用底层的tryLock。伪代码逻辑是这样的public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) { // 判断总等待时间 // 基于当前时间计算剩余可用时间 for (RLock lock : locks) { long remainTime ...; // 剩余等待时间 boolean res lock.tryLock(remainTime, leaseTime, unit); if (!res) { // 加锁失败回滚所有已获取的锁 unlockInner(Arrays.asList(locks.subList(0, index))); return false; } } return true; }每个底层锁的tryLock走到最后是执行 Lua 脚本if (redis.call(exists, KEYS[1]) 0) then redis.call(hset, 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]);这是 Redisson 单锁的可重入加锁脚本。到了 MultiLock 层面这段脚本会被每个节点独立执行锁的持有者标识、可重入计数、过期时间各自独立。这里有一个细节加锁不是并发的是串行遍历。Redis 客户端对每个节点单独发命令一个节点一个节点地试。这也是 MultiLock 性能不如单锁的直接原因。如果要锁的节点数量多加锁耗时线性上涨waitTime 必须给足。3.2 加锁接口参数leaseTime、waitTime 与重试掌握 MultiLock 的tryLock参数基本就掌握了它的性格。leaseTime是锁的自动释放时间单位毫秒。如果不传 leaseTime 或者传 -1Redisson 会启动看门狗机制默认续期 30 秒每 10 秒续期一次。MultiLock 和普通锁一样也支持看门狗但行为有差异这点后面单讲。waitTime是最多等待多久。MultiLock 的总等待时间不是所有节点等待时间之和而是在整个调用周期内共享的预算。假设 waitTime 是 3 秒第一个锁花了 2 秒才拿到那么第二个锁最多还能等 1 秒第三个锁几乎不可能有机会尝试。源码里通过System.currentTimeMillis()计算每个锁的剩余等待时间就是这个逻辑。这就带来一个重要的实操指导MultiLock 的 waitTime 不能设置得太小。如果节点多、网络抖动前面几个节点消耗了大部分等待时间后面的节点直接没机会尝试。我在实测中倾向于把 waitTime 设为单锁的 1.5 到 2 倍。重试机制方面Redisson 默认的tryLock在锁被占用时会返回 false不会自动无限重试。如果你需要重试业务层要自己循环调用并且要注意重试之间做一点间隔避免无意义地打爆 Redis。3.3 释放锁的机制与遍历解锁释放锁时MultiLock 也是遍历所有锁逐个执行 unlock。每个锁资源独立删除任何一个节点释放失败Redisson 的默认策略是继续尝试释放其他节点。这里有个实际工程问题如果某个节点在加锁成功后、释放前发生故障这把锁只能等 leaseTime 到期自动过期。MultiLock 在这方面和单锁一样没有魔法最终一致性依赖 Redis 的 key 过期机制。释放锁的实现逻辑如下public void unlock() { // 遍历所有锁逐个释放 for (RLock lock : locks) { lock.unlock(); } }如果中途某个节点 unlock 抛异常Redisson 会中断释放吗源码层面的处理值得注意MultiLock 的 unlock 方法里对每个锁依次调用异常会往上抛但不会回滚已经执行过的解锁动作。也就是说已经释放的节点会保持释放状态未释放的节点可以再手动处理。如果你在业务代码里遇到 unlock 异常不要重复执行整个 MultiLock 的 unlock否则已经释放的锁会再次试图释放可能抛锁不存在的异常。3.4 看门狗在 MultiLock 下的表现看门狗是 Redisson 的特色机制它会在锁的 leaseTime 到期前自动续期。对单锁来说这个机制很简单底层有一个定时任务每 10 秒检查一次锁是否还存在如果存在就重新设置过期时间。MultiLock 的看门狗行为我没法用每个锁都有独立看门狗来简单概括。Redisson 的 MultiLock 内部维护了一个ScheduleService在加锁时如果没有指定 leaseTime看门狗会被启动并周期性地为所有已加锁的锁批量续期。你需要知道的关键点是看门狗为 MultiLock 续期时续期的前提是锁还有效。如果某个锁已经因为异常丢失比如 Redis 节点宕机看门狗在续期时发现 key 不存在它会停止这个锁的续期但不会自动回滚其它锁。这可能导致部分锁被续期部分锁已经失效。这个行为在实际故障中会造成锁的半失效状态业务层看起来 MultiLock 还在但实际部分底层锁已经不存在了。要应对这种情况方案是给加法传入明确的 leaseTime而不是完全依赖看门狗。明确的 leaseTime 配合业务执行时间上限能避免锁还在但实际已丢失的尴尬局面。4. 典型案例用 MultiLock 保证跨库/跨实例一致性4.1 典型业务场景最典型的场景是一个业务操作需要同时修改两个独立系统中的数据这两个系统各自有一套 Redis 锁业务要求要么同时操作要么都不操作。举个例子某开发者在电商系统里做了一个库存和优惠券的组合扣减操作。库存服务使用 Redis A 的锁优惠券服务使用 Redis B 的锁。普通的做法是分别在两个服务里加锁但这样无法保证整体原子性库存锁拿成功优惠券锁拿失败库存已经被扣了优惠券还没扣业务就处于中间状态。改成 MultiLock 之后逻辑变成先尝试同时获得库存锁和优惠券锁全部成功后才执行扣减任何一个失败就直接释放已获得的锁整体不执行。这样就从锁层面避免了中间状态。4.2 MultiLock 与普通锁对比用一个表格直观对比维度普通 RLockMultiLock锁的对象单个 Redis key多个独立 Redis key加锁成功条件单个 key 成功所有 key 全部成功失败处理直接返回失败释放已获得的锁整体失败性能高一次网络交互低N 次串行网络交互适用场景单资源互斥多资源组合操作的原子保护故障容忍单点故障锁失效提高整体可用性但非绝对这里的非绝对需要强调MultiLock 并不能完全消除故障场景下的锁失效问题它只是把多个独立节点的故障概率相乘。只要不是所有节点同时出问题锁状态就是安全的。但如果所有节点同时不可用MultiLock 一样崩溃。4.3 实操配置注意事项waitTime 的取舍实操中MultiLock 参数配置是最容易踩坑的地方。我建议的配置口径如下leaseTime必须设置且要大于业务最大执行时间。如果业务执行时间不确定至少设置一个合理上限比如 30 秒或 60 秒。不要完全依赖看门狗。waitTime设为单锁的 1.5 到 2 倍。如果业务并发高适当在加锁失败后增加 sleep 重试。锁数量控制在 3 到 5 个以内。超过这个数量加锁耗时和等待时间会不成比例地膨胀。下面是一个典型的调用示例long waitTime 3000L; long leaseTime 30000L; boolean locked multiLock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS); if (locked) { try { // 执行业务操作 doBiz(); } finally { multiLock.unlock(); } } else { // 加锁失败走降级逻辑 handleLockFailure(); }这里面最容易被忽略的是leaseTime的语义如果业务执行超过 leaseTime锁会自动释放其他线程就能进入临界区而此时业务可能还没结束造成并发冲突。所以 leaseTime 必须认真评估不要随手写个 10 秒就完事。5. 常见误区与踩坑实录5.1 误区MultiLock 一定比单锁安全这是最常见的认知偏差。MultiLock 只是把锁的可靠性从单点提升到了多点并没有改变分布式锁最根本的困境网络分区、进程暂停、时钟跳跃。举一个我在调研 Redlock 时看到的经典反例线程 T1 获取了 MultiLock 的所有锁执行过程中发生了长时间的 GC 停顿超过了 leaseTime锁自动过期。另一个线程 T2 获取了同样的锁开始执行业务。T1 GC 恢复后继续执行两个线程同时进入临界区。MultiLock 在这种情况下无能为力因为它依赖的仍然是 Redis 的过期时间而 GC 停顿是应用层面的不可控因素。所以MultiLock 适合解决节点故障维度的锁失效问题但不适合作为业务执行时间绝对可靠的保证。关键业务还是要依赖幂等、版本号、乐观锁等手段做兜底。5.2 故障场景部分节点加锁失败线上最容易遇到的问题是部分节点加锁失败后业务日志里有unlockattempt to unlock lock, not locked by lock owner之类的异常。这个异常通常不是业务逻辑错误而是时序问题。假设线程 T1 通过 MultiLock 加锁A 成功、B 失败。MultiLock 内部回滚 A。但在回滚的瞬间另一个线程 T2 可能已经获取了 A 的锁。这时 T1 的回滚动作如果被误判为释放自己持有的锁就会与 T2 产生冲突。Redisson 源码通过 Lua 脚本来校验持有者标识只有持有者标识匹配时才删除 key。这保证了回滚操作不会误删别人的锁。但异常依然会冒出因为回滚时 T2 可能已经持有锁T1 的 unlock 动作在 T2 的视角里是第三方操作会被拒绝。排查这个问题的正确姿势是把加锁失败的日志和回滚日志打点确认失败发生在哪一个节点。不要简单重试整个 MultiLock因为重试周期内可能会产生同样的争抢。5.3 leaseTime 一致性、线程问题与重试风险另一个高频坑是 leaseTime 在各节点之间配置不一致。MultiLock 的每个底层锁可以独立传入 leaseTime但你几乎不应该这么做。如果 A 的 leaseTime 是 30 秒B 的 leaseTime 是 10 秒B 先过期其他线程就能通过 B 进入临界区MultiLock 的同时存在语义就被破坏了。线程问题方面MultiLock 要求加锁和解锁必须由同一个线程完成。Redisson 的锁内部会记录持有者标识这个标识与线程绑定。如果你在异步回调里解锁很可能出现当前线程不是锁持有者的报错。我见过有人把tryLock放在一个线程把unlock放在另一个线程结果锁永远解不开直到过期。MultiLock 的每个底层锁都是同样的约束。重试风险则体现在如果你在业务层面循环尝试 MultiLock锁竞争激烈时多次加锁失败会反复触发回滚和释放导致 Redis 网络开销剧增。建议设置最大重试次数比如 3 次超过后直接走降级流程而不是无限循环。6. 我个人的落地建议结合我看过的源码和实际踩坑经历落地 MultiLock 时我会按下面的原则来第一不要轻易上 MultiLock。大部分业务场景的单资源互斥用 Redisson 单锁就够了。单锁的可靠性问题主要集中在主从切换窗口这个概率并非每天发生。如果业务对极端一致性有硬性要求再去考虑多节点组合锁。第二如果决定使用 MultiLock锁节点的数量不要贪多。三个独立节点已经能显著降低整体故障概率。再加更多节点收益递减代价却线性上升。第三leaseTime 必须显式设置不要依赖看门狗。我在交易系统里的做法是根据业务的最大执行时间评估再往上加 30% 的余量。如果业务偶尔超时宁可让锁提前释放后靠业务幂等兜底也不要让锁长时间占用坑了其他请求。第四加锁失败后的回滚流程一定要在最终代码里体现为释放之前已经成功的锁而不是简单地打印错误。依托 Redisson 的内部回滚机制加上完整链路日志能让你在故障复盘时迅速定位问题发生在哪个节点。MultiLock 是一个好工具但分布式锁本质上没有银弹。理解它的设计动机和缺陷边界比记住几个 API 更重要。你在动手接入之前先想清楚两个问题我的业务真的需要多个独立节点同时加锁吗如果其中一个节点挂了我的系统能承担锁失效的后果吗想清楚这两点再下手写代码不迟。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑