引言
本期我们将把目光聚焦在 Redisson 中另一个颇具代表性的分布式锁实现------MultiLock。它的核心思想是:一次性对多个独立的 RLock 进行加锁或解锁操作,只有当多个锁都成功加锁时才算真正完成锁的获取,一旦有任何一个失败,整体操作都会回滚。这种"整锁整放"的方式,能更好地满足某些高要求的分布式业务场景。
介绍
在分布式环境中,如果我们将数据拆分到不同的 Redis 实例、集群或是不同的 key 上,有时会遇到需要"一次性对 N 个资源都上锁,才算占用资源"的场景。使用 Redisson 的 MultiLock
可以极大地简化这类需求的实现。它提供了一个整合多个 RLock
的抽象,对外暴露成单一的锁接口,使用起来就像在操作一把锁,而内部却是对多把锁的组合操作。
它的典型应用场景包括:
- 同时锁定多个不同 Redis key,保证"要么全加锁成功,要么全部不加锁"。
- 跨多个机房 / Redis 节点时的互斥需求,尤其在做异步、分布式任务调度时,减少了操作多个锁的繁琐。
接下来让我们直击源码,看一下 RedissonMultiLock
(简称 MultiLock)是如何实现这一套逻辑的。
加锁
在 Redisson 源码里,MultiLock
的主要实现类是 org.redisson.RedissonMultiLock
。其核心属性是一个 List<RLock>
,用来保存所有需要一起加锁的锁。它本质上也是一个 java.util.concurrent.locks.Lock
,所以有类似的 lock()
和 tryLock()
等方法。
下面截取关键的加锁逻辑(为方便说明,做了适当精简):
java
public class RedissonMultiLock implements Lock {
private final List<RLock> locks = new ArrayList<>();
public RedissonMultiLock(RLock... locks) {
this.locks.addAll(Arrays.asList(locks));
}
@Override
public void lock() {
lock(-1, null);
}
@Override
public void lock(long leaseTime, TimeUnit unit) {
boolean locked = tryLock(leaseTime, unit);
if (!locked) {
throw new IllegalStateException("Unable to acquire lock");
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
long startTime = System.currentTimeMillis();
List<RLock> acquiredLocks = new ArrayList<>();
for (RLock lock : locks) {
// 计算剩余的等待时间
long elapsed = System.currentTimeMillis() - startTime;
long remain = time - elapsed;
if (remain <= 0 && time != -1) {
// 超时了,回滚所有已获取的锁
unlockInner(acquiredLocks);
return false;
}
// 尝试获取锁
boolean success;
if (time == -1) {
success = lock.tryLock(); // 不限等待时间
} else {
success = lock.tryLock(remain, unit);
}
if (!success) {
// 获取锁失败,回滚
unlockInner(acquiredLocks);
return false;
}
acquiredLocks.add(lock);
}
return true;
}
private void unlockInner(List<RLock> locks) {
for (RLock lock : locks) {
try {
lock.unlock();
} catch (Exception e) {
// 异常处理,通常记录日志或忽略
}
}
}
// ...
}
从这段代码中,我们可以看到:
- MultiLock 会将"要一起加锁"的多个
RLock
封装进一个List
; - 当调用
tryLock
时,会挨个尝试获取每个RLock
; - 若所有锁都获取成功,才返回
true
; - 如果中途有任何一个锁获取失败或者超时,就会调用
unlockInner
方法,依次释放已成功获取的锁,回滚到初始状态,保持分布式环境下的原子性。
这样就保障了"要么所有锁都成功加锁,要么一个都不会留存",消除了状态不一致的风险。
释放锁
MultiLock 的释放逻辑同样是"全部释放"或"都不释放"。来看下相关核心方法 unlock()
的实现(截取简化版本):
java
public void unlock() {
// 这里我们是一次性 unlock 所有 locks
for (RLock lock : locks) {
try {
lock.unlock();
} catch (Exception e) {
// 可能出现解锁异常,比如锁不属于当前线程等情况,做必要处理
}
}
}
可以看到,unlock()
内部直接遍历所有 RLock
并执行解锁操作。这意味着无论中途某个锁因为"非本线程占用"等原因导致报错,其余锁也会继续解锁,力求释放尽可能多的锁,尽量避免"部分锁未释放"造成死锁风险。
以此也体现了 MultiLock 的一贯思路:要么全部锁住,要么全部释放,让多个分布式锁在逻辑上"捆绑"成一体。
如何保证"一致性"?
- 获取失败即回滚
当调用tryLock
时,如果期间任何一个分布式锁无法加锁成功,就立即回滚(释放已获取的锁)。这是确保多锁原子性的关键。 - 重复可重入语义仍需依赖具体 RLock
如果多个RLock
中有些是可重入锁,那么在同一线程下反复获取时,并不会阻塞。MultiLock 并不会额外重写可重入逻辑,它更多地是一个"协调器",背后依然由各个RLock
自身的 reentrant 实现来支撑。 - 统一的超时控制
tryLock(long time, TimeUnit unit)
会逐一减少剩余可用时间,避免因为某个锁获取太慢导致整个流程卡死。 - 释放过程对每个锁都负责
哪怕出现解锁异常,MultiLock 也会继续释放其他锁,将风险与影响降至最低。
小结
RedissonMultiLock
通过将多把 RLock
打包成一个"组合锁",让使用者在编程时只需关心"我拿到所有锁了吗?所有锁都释放了吗?"。它背后通过遍历加锁并回滚的策略,保证了原子性,避免了分布式环境下常见的"锁定不一致"问题。
与之前的公平锁等其他锁实现相比,MultiLock 并不是通过 Lua 脚本在单个 Redis 实例上实现的,而是通过对多个锁对象的封装来保证"一起成功或一起失败"。它更多用于满足"一次性锁定多资源"的场景,这比单一锁更适用分布式业务中对一致性、原子性要求更高的场景。
希望本文能帮助大家厘清 MultiLock 的实现原理。与其余 Redisson 锁一样,阅读源码的过程能让我们更好地理解其在分布式场景下如何保证安全与高效,也能启发我们在设计自定义分布式组件时,如何通过"组合"思维来化繁为简。我们下一期再见!