Redisson 里 MultiLock 怎么加锁和释放,源码细节拆解聊聊
- 问答
- 2026-01-04 17:31:20
- 15
MultiLock 是什么
简单说,MultiLock 就是把多个独立的 RLock 锁(lock1, lock2, lock3)捆绑在一起,当作一个锁来用,它的核心目标是保证在分布式环境下,对所有捆绑的锁进行原子性的操作,要么一次性全部加锁成功,要么全部失败,防止死锁,这在需要同时锁定多个资源的事务场景中非常关键。
加锁过程源码拆解
加锁的入口方法是 lock(),但核心逻辑在 tryLockInnerAsync 方法中,我们一步步跟进去看。
-
入口与参数准备 当我们调用
multiLock.lock()时,最终会调用到RedissonMultiLock类的tryLock方法,这个方法会循环尝试获取锁,真正的重头戏是一个叫tryLockInnerAsync的异步方法,这个方法会向所有 Redis 节点发送一段 Lua 脚本,Lua 脚本是 Redis 执行原子操作的关键。 -
核心 Lua 脚本分析 这是加锁最精华的部分,脚本很长,但我们可以拆开看它的逻辑,脚本接收几个参数:锁的过期时间
leaseTime、每个锁对应的客户端 ID(Redisson 会用 UUID 和线程 ID 组合成一个唯一标识)。- 第一步:声明一个结果表。 脚本一开始会创建一个表(比如叫
result)来存储每个锁操作的结果。 - 第二步:循环处理每一个锁。 脚本会用一个循环,依次处理 MultiLock 中包裹的每一个 RLock 对象。
- 第三步:单锁加锁逻辑。 对于循环中的每一个锁,它会执行和单锁类似的命令:
redis.call('hset', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);这里KEYS[1]是当前循环到的锁的键名,ARGV[2]是客户端 ID,ARGV[1]是锁的过期时间,这个命令的意思是:在 Redis 的 Hash 数据结构中,设置这个锁键,字段是客户端 ID,值是 1(这个值用于可重入计数),然后给这个锁键设置一个过期时间。 - 第四步:收集结果。 将上一步
hset命令的结果(如果键是新建的返回 1,如果是已存在返回 0)收集到之前声明的result表中。 - 第五步:计算总耗时。 在循环开始前记录当前时间,循环结束后再记录一次,用这两个时间计算出给所有锁发送命令的总耗时。
- 第六步:检查剩余有效期。 这是非常关键的一步!脚本会检查计算出的总耗时,然后用传入的锁过期时间
leaseTime减去这个总耗时,得到remainingLiveTime(剩余存活时间)。 - 第七步:统一过期时间。 如果剩余存活时间小于等于 0,说明在加锁过程中,最先被设置的那个锁可能已经快过期甚至已经过期了,这非常危险,脚本会遍历之前的结果表,对任何已经加锁成功的锁执行删除操作(
del命令),确保所有锁都不会被留下,然后返回 nil,表示加锁失败。 - 第八步:返回成功。 如果剩余存活时间大于 0,说明所有锁都成功设置,并且它们至少还有
remainingLiveTime这么长的有效期,此时脚本返回这个剩余时间。
- 第一步:声明一个结果表。 脚本一开始会创建一个表(比如叫
-
客户端处理结果 客户端的 Java 代码拿到 Lua 脚本的返回值,如果返回值不是 nil,就表示加锁成功,这个返回值(剩余存活时间)还会被用来在客户端启动一个看门狗(Watchdog)线程,这个线程会在锁过期前定期(比如每隔过期时间的1/3)去续期,从而实现类似“可重入锁”的无限等待效果。
释放锁过程源码拆解
释放锁的入口是 unlock(),核心同样在 unlockInnerAsync 方法的 Lua 脚本里。
-
核心 Lua 脚本分析 解锁脚本同样会循环处理 MultiLock 中的每一个锁。
- 第一步:声明计数器。 脚本会初始化两个计数器:
count(总共成功释放的锁数量)和failedLocks(释放失败的锁列表,记录其索引)。 - 第二步:循环处理每一个锁。 和加锁一样,遍历所有锁。
- 第三步:单锁释放逻辑。 对于每个锁,执行单锁的释放逻辑:
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);这里ARGV[3]还是客户端 ID,这个命令是将 Hash 中该客户端 ID 对应的值减 1,这个值就是重入次数。- 如果减 1 之后的值大于 0,说明这个锁还被当前线程重入持有着,那么就给这个锁重新设置一下过期时间。
- 如果减 1 之后的值等于 0,说明这个锁已经彻底释放了(没有重入了),那么就直接删除(
del)这个锁键。
- 第四步:处理结果。 如果上一步的
del命令成功执行(返回 1),或者在重入情况下续期成功,就算这个锁释放操作成功,count计数器加 1,如果操作失败(比如锁不存在,或者锁不属于当前客户端),就把这个锁在 MultiLock 中的索引号记录到failedLocks列表中。 - 第五步:返回结果。 循环结束后,脚本返回一个表,里面包含三个信息:
count(成功数)、failedLocks(失败锁索引列表)、ARGV[3](客户端 ID)。
- 第一步:声明计数器。 脚本会初始化两个计数器:
-
客户端处理结果 客户端的 Java 代码拿到脚本返回的结果后,会进行检查:
count的值等于锁的总数,说明所有锁都释放成功,那么解锁操作就完成了。failedLocks列表不为空,说明有些锁释放失败了(比如这个锁本来就不属于当前客户端,或者已经被意外删除了),Redisson 会抛出一个异常,通知用户释放锁时发生了问题。
总结一下关键点
通过源码拆解,我们可以看到 MultiLock 的设计精髓:
- 原子性幻觉: 它通过一个 Lua 脚本“几乎原子性”地完成所有锁的操作,虽然命令在网络上还是逐个发送的,但服务器端脚本的执行是原子的,并且脚本内部逻辑保证了“全成功”或“全失败”的语义。
- 耗时补偿: 加锁脚本中计算总耗时并调整剩余存活时间的设计非常巧妙,它避免了因为网络延迟导致先加的锁过早过期的问题。
- 批量操作: 无论加锁还是解锁,都是通过一次网络通信(执行一个 Lua 脚本)完成对所有锁的操作,减少了网络开销,提高了性能。
- 状态一致性: 解锁后对结果的检查确保了资源状态的一致性,如果部分锁释放失败,会通过异常提醒使用者,防止出现不可预知的行为。
Redisson 的 MultiLock 并不是一个真正的“原子锁”,而是通过精巧的 Lua 脚本和客户端逻辑,在分布式环境下模拟出了原子锁的行为,从而安全地解决了多个资源同时锁定的难题。

本文由帖慧艳于2026-01-04发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/74456.html
