目录
1. Redisson解决不可重入锁导致的死锁问题
2. 不可重试问题
Pub/Sub 的优势
锁释放的发布逻辑
3. 超时释放的问题
1. 锁的超时释放机制背景
2. 源码分析
2.1 锁的获取
2.2 看门狗机制
2.3 看门狗续期实现
2.4 手动设置锁的过期时间
总结
4. 主从一致性
问题背景
解决方案:使用 MultiLock
使用场景:
MultiLock 的核心思路
2. MultiLock 的结构
关键成员变量:
3. 获取锁 (lock 方法)
4. 解锁操作 (unlock 方法)
锁获取失败时的处理
锁的自动释放
关键代码
详细解释
为什么需要重新设置过期时间?
总结
1. Redisson解决不可重入锁导致的死锁问题
不可重入锁是指,某个线程获取锁后,在锁释放之前,如果该线程再次尝试获取该锁,则会阻塞自己,从而可能导致死锁问题。
解决方法: Redisson 提供了可重入锁(RLock
),支持同一线程多次加锁,锁的计数器会累加。每次解锁时,计数器减少,直到计数器归零才释放锁
使用HashMap的形式,记录每个线程获取锁的次数
2. 不可重试问题
在分布式环境下,多个客户端同时尝试获取同一把锁时:
- 如果直接轮询(频繁重试),会导致 Redis 的压力过大(高频请求)。
- 如果直接返回失败,则无法利用锁的释放时机,可能会错失锁。
解决方法:在 Redisson 中,通过 tryLock
方法实现了分布式锁的 不可重试 问题的优化。Redisson 的解决方法并不是简单的立即返回失败或者频繁重试,而是利用 Redis 的 发布-订阅(Publish-Subscribe)机制 来高效监听锁释放事件。
在第一次获取锁失败之后,并不是立即返回失败,也不是立刻重新获取锁给CPU造成负担,而是基于一种“观察者”的形态,订阅别人释放锁的信息——Publish-subscribe。
Redisson 使用 Redis 的发布-订阅机制,监听锁的释放事件,从而实现高效的等待锁。
源码位置: 在 Redisson 的 RedissonLock.tryLock
方法中,核心逻辑是:
- 首先尝试获取锁。
- 如果获取锁失败,订阅锁释放事件(通过 Redis 的 Pub/Sub 机制)。
- 等待锁释放通知,再次尝试获取锁。
获取锁 的逻辑
以下是主要的逻辑源码片段(简化版):
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long currentTime = System.currentTimeMillis();
// 尝试获取锁(首次)
if (tryAcquire(leaseTime, unit)) {
return true; // 成功直接返回
}
// 订阅锁释放事件
RFuture<RedissonLockEntry> subscribeFuture = subscribeToChannel();
try {
if (!await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
return false; // 等待超时,返回失败
}
// 循环监听锁的释放事件,直到成功获取锁或超时
while (time > 0) {
// 再次尝试获取锁
if (tryAcquire(leaseTime, unit)) {
return true;
}
time -= waitForMessage(time); // 等待锁释放通知
}
} finally {
unsubscribeFromChannel(); // 解订阅
}
return false; // 获取锁失败
}
结合源码,以下是 tryLock
的具体执行流程:
-
尝试获取锁(非阻塞):
- Redisson 首次尝试调用
tryAcquire
方法,直接获取锁。 - 如果成功,则返回
true
,完成加锁。 - 如果失败,说明锁已被其他线程持有。
- Redisson 首次尝试调用
-
订阅锁释放事件:
- 调用
subscribeToChannel
方法,在 Redis 的一个频道上订阅锁的释放事件。 - Redisson 为每个锁定义一个唯一的频道名,例如
__keyevent@0__:del
。 - 当锁被释放时,会通过 Redis 的
PUBLISH
命令通知订阅的客户端。
- 调用
-
等待锁释放通知:
- 如果订阅成功,Redisson 会阻塞当前线程,等待锁释放的消息。
- 这个阻塞等待由
await
方法实现,监听锁释放信号。 - 当其他客户端释放锁时,Redis 会发布消息,Redisson 收到后唤醒线程。
-
再次尝试获取锁:
- 收到锁释放通知后,Redisson 再次尝试调用
tryAcquire
方法获取锁。 - 如果成功获取,则返回
true
。 - 如果仍然失败(可能被其他线程抢占),继续循环等待,直到超时。
- 收到锁释放通知后,Redisson 再次尝试调用
-
超时处理:
- 如果在指定的等待时间内没有成功获取锁,Redisson 会退出循环,解订阅锁释放事件,返回
false
。
- 如果在指定的等待时间内没有成功获取锁,Redisson 会退出循环,解订阅锁释放事件,返回
Pub/Sub 的优势
这种基于发布-订阅机制的实现方式,解决了锁竞争时的资源浪费问题:
- 避免频繁轮询: 客户端不会反复尝试请求锁,而是阻塞等待锁的释放通知,降低 Redis 的压力。
- 实时性高: 一旦锁释放,Redis 会立即发布消息通知所有订阅的客户端,客户端可以快速响应。
- 效率高: 使用
await
方法阻塞线程,减少 CPU 资源消耗。
锁释放的发布逻辑
锁的释放通过 Redis 的 PUBLISH
命令触发。以下是锁释放的逻辑:
@Override
public void unlock() {
// 删除锁键
get(commandExecutor.writeAsync(getRawName(), RedisCommands.DEL, getRawName())).thenAccept(deleted -> {
if (deleted > 0) {
// 通知订阅的客户端锁已释放
publishUnlock();
}
});
}
publishUnlock
方法:
private void publishUnlock() {
// 发布释放事件到 Redis 频道
commandExecutor.get(commandExecutor.writeAsync(getChannelName(), RedisCommands.PUBLISH, getChannelName(), LOCK_MESSAGE));
}
在 Redisson 的 tryLock
中,基于 Pub/Sub 机制实现了锁的高效等待:
- 第一次尝试获取锁失败后,不是立即返回失败,而是通过订阅锁释放的事件,类似一个“观察者”监听锁的状态。
- 收到锁释放通知后,立即尝试再次获取锁,从而避免了频繁轮询和资源浪费。
3. 超时释放的问题
1. 锁的超时释放机制背景
当线程获取锁后,如果线程意外退出或执行时间超过锁的过期时间,Redis 中的锁键会被自动删除,从而释放锁。
问题:
- 如果业务逻辑执行时间较长,可能超过锁的过期时间,导致锁被释放,而其他线程获取到锁,导致数据不一致。
- 如果锁没有正确释放,也会造成资源被永久占用(死锁)。
解决方案:
- Redisson 通过看门狗机制动态延长锁的过期时间,避免锁意外释放。
- 用户也可以手动指定锁的过期时间,避免看门狗机制。
2. 源码分析
2.1 锁的获取
Redisson 分布式锁的核心是 Redis 的键值存储。锁被表示为一个 Redis 键,其值是锁的拥有者标识(如 UUID:ThreadId
),并设置过期时间。
源码位置:RedissonLock.tryAcquire
private boolean tryAcquire(long leaseTime, TimeUnit unit) {
long threadId = Thread.currentThread().getId();
String lockValue = getLockValue(threadId); // 生成 UUID + ThreadId 的唯一标识
// 通过 Redis 的 SET NX 设置锁,如果设置成功则返回 1
RFuture<Boolean> future = commandExecutor.writeAsync(
getRawName(),
RedisCommands.SET,
getRawName(),
lockValue,
"NX",
"PX", unit.toMillis(leaseTime)
);
return get(future);
}
逻辑分析:
- 调用 Redis 的
SET NX
指令:NX
:表示仅当键不存在时才设置值。PX
:设置键的过期时间(以毫秒为单位)。
- 如果设置成功(返回 1),表示当前线程成功获取到锁。
- 如果失败,表示锁已被其他线程持有。
2.2 看门狗机制
看门狗机制的作用是:在锁未被释放前,动态延长锁的过期时间,防止业务逻辑超时导致锁提前释放。
当 TTL = -1
时:(用户没有指定锁的过期时间)
- 锁的生存时间依赖于 Redisson 的看门狗机制,而不是 Redis 自己的超时机制。
- 看门狗会定期续约锁,确保锁不会因超时被释放,直到显式调用
unlock
方法来释放锁。
@Override
public void lock() {
lock(-1, null); // -1 表示使用默认过期时间
}
private void lock(long leaseTime, TimeUnit unit) {
long threadId = Thread.currentThread().getId();
try {
// 尝试加锁
boolean success = tryAcquire(leaseTime, unit);
if (!success) {
// 等待锁的释放或继续重试
awaitLock(leaseTime, unit, threadId);
}
// 如果使用看门狗机制,启动续期任务
if (leaseTime == -1) {
scheduleWatchdogRenewal(threadId);
}
} catch (Exception e) {
// 异常处理
}
}
源码位置:RedissonLock.lock
关键逻辑:
- 如果用户未指定锁的过期时间(
leaseTime == -1
),Redisson 启动看门狗机制,默认的锁超时时间为 30秒。 - 启动一个定时任务,周期性延长锁的过期时间,确保锁不会意外释放。
2.3 看门狗续期实现
源码位置:scheduleWatchdogRenewal
private void scheduleWatchdogRenewal(long threadId) {
commandExecutor.getConnectionManager().newTimeout(timeout -> {
// 检查锁是否仍然由当前线程持有
if (isHeldByCurrentThread(threadId)) {
// 发送 Redis 的 EXPIRE 命令,延长锁的过期时间
commandExecutor.writeAsync(getRawName(), RedisCommands.PEXPIRE, getRawName(), LOCK_EXPIRATION_TIME);
// 再次调度续期任务
scheduleWatchdogRenewal(threadId);
}
}, LOCK_WATCHDOG_CHECK_INTERVAL, TimeUnit.MILLISECONDS);
}
关键逻辑:
-
锁状态检查:
- 调用
isHeldByCurrentThread
方法,确保当前线程仍然持有锁。 - 如果锁已经被释放,则终止续期任务。
- 调用
-
延长锁的过期时间:
- 使用 Redis 的
PEXPIRE
指令,将锁的过期时间延长到默认的 30秒。 - 每次续期时,锁的过期时间会被重置。
- 使用 Redis 的
-
递归调度续期任务:
- 每隔 10秒(默认) 检查一次锁的状态,如果锁未释放,则继续延长过期时间。
2.4 手动设置锁的过期时间
如果用户明确知道业务逻辑的执行时间,可以手动设置锁的过期时间,禁用看门狗机制。
代码示例:
lock.lock(10, TimeUnit.SECONDS); // 持有锁 10 秒,10 秒后自动释放
源码位置:RedissonLock.lock
private void lock(long leaseTime, TimeUnit unit) {
long threadId = Thread.currentThread().getId();
try {
boolean success = tryAcquire(leaseTime, unit);
if (!success) {
awaitLock(leaseTime, unit, threadId);
}
// 如果用户指定了过期时间,不启动看门狗机制
if (leaseTime > 0) {
return;
}
// 启动看门狗机制
scheduleWatchdogRenewal(threadId);
} catch (Exception e) {
// 异常处理
}
}
总结
Redisson 的超时释放机制主要通过以下方式实现:
-
默认看门狗机制:
- 当用户未设置锁的过期时间时,Redisson 启动看门狗任务,每隔 10秒 检查一次锁是否需要续期,延长过期时间,确保锁不会意外释放。
- 默认过期时间为 30秒,每次续期重置为 30 秒。
-
手动设置过期时间:
- 用户可以显式指定锁的持有时间,禁用看门狗机制。锁会在指定时间到期后自动释放。
-
Redis 的关键指令:
SET NX PX
:设置锁并指定初始过期时间。PEXPIRE
:动态更新锁的过期时间。
Redisson 的这一机制在避免锁超时问题的同时,也能通过手动控制灵活应对不同业务场景。
超时续约:利用watchD0g,每隔一段时间(releaseTime / 3),重置超时时间。这个超时续约只有-1才执行,也就是说自己设置超时时间是没有执行回调(可以倒回去看源码),所以这个续约是防止java代码宕机,redis还有锁的id的不是解决执行业务超时
4. 主从一致性
Redisson的multiLock解决Redis主从关系,主节点宕机导致主从不一致,锁丢失的问题
Redisson的 MultiLock
机制可以用于解决Redis主从关系中主节点宕机导致主从不一致和锁丢失的问题。具体来说,MultiLock
可以通过组合多个锁来保证原子性,从而在多个 Redis 实例之间管理分布式锁的状态,避免在主节点宕机时锁丢失
问题背景
在 Redis 主从架构中,当主节点宕机时,可能会导致一些分布式锁的丢失或状态不一致的问题。尤其是在多实例环境下,主节点和从节点之间的复制延迟或数据同步不及时,可能会导致某些操作无法在多个节点间正确协调,进而影响数据一致性和锁的管理。
解决方案:使用 MultiLock
Redisson 提供了 MultiLock
机制,可以让你在多个 Redis 实例之间设置多个锁。它支持多个锁对象(比如 Redis 的多个 key)在一个操作中共同参与,确保这些锁的操作具备原子性。
使用场景:
- 跨多个 Redis 实例的分布式锁:如果你的架构包含多个 Redis 实例(如主从架构),你可能希望在主从节点之间确保一个分布式锁不会在主节点宕机后丢失。此时,你可以使用
MultiLock
来跨多个 Redis 实例管理锁。 - 锁丢失防止:即使 Redis 的主节点宕机或发生故障,
MultiLock
会确保在主节点和从节点之间锁的持有一致性,避免因主节点不可用导致锁状态丢失。
MultiLock
的核心思路
MultiLock
的目标是提供一个组合锁,它通过同时获取多个锁来保证操作的原子性。当多个锁被封装在 MultiLock
中时,它们的所有锁操作(如获取锁、释放锁)会以一个整体来执行,确保多个锁在一个事务中操作。
2. MultiLock
的结构
MultiLock
本质上是封装了多个 RLock
锁对象,并提供了一些操作方法来确保这些锁的原子性和一致性。
关键成员变量:
MultiLock
主要有一个 locks
集合来保存多个锁对象。
private final List<RLock> locks;
3. 获取锁 (lock
方法)
当调用 multiLock.lock()
时,它会执行以下逻辑:
-
遍历所有锁: 它会遍历
locks
集合中的每一个RLock
对象,尝试获取每个锁。 -
获取锁: 对于每个锁对象,调用
RLock
的lock
方法。这是通过 Redis 的SETNX
操作来实现的,即使得每个锁在 Redis 中以原子方式被获取。 public void lock() { List<Thread> threads = new ArrayList<>(); for (RLock lock : locks) { Thread thread = new Thread(() -> { try { lock.lock(); } catch (InterruptedException e) { // 异常处理 } }); thread.start(); threads.add(thread); } for (Thread thread : threads) { thread.join(); } }
lock()
方法的主要职责是获取MultiLock
所包含的所有子锁(RLock
)的锁定。这是通过为每个子锁启动一个独立的线程来并行获取锁,然后等待所有线程完成锁定操作。- 调用
join()
方法:对于threads
列表中的每个线程,调用thread.join()
,这会使当前线程(调用lock()
的线程)等待直到该锁定线程完成执行。 - 确保所有子锁都已获取:只有在所有子锁都成功获取后,
lock()
方法才会返回,从而保证MultiLock
已经成功锁定了所有相关资源。
- 在这个过程中,如果任何一个锁无法获取(例如超时或者其他原因),它会尝试释放所有已经获取的锁。
- 原子操作: 为了保证多锁获取的原子性,
MultiLock
必须在所有锁都成功获取后才认为操作完成。这样,只有所有锁都被成功获取,才能继续执行接下来的操作。
4. 解锁操作 (unlock
方法)
调用 multiLock.unlock()
时,会遍历所有的 RLock
,释放它们所持有的锁:
public void unlock() {
for (RLock lock : locks) {
lock.unlock();
}
}
每个 RLock
的 unlock()
会执行 Redis 的 DEL
操作,释放该锁。这保证了锁能够在多节点分布式环境下及时释放,防止死锁。
锁获取失败时的处理
如果在尝试获取锁时遇到失败(例如超时或中断),MultiLock
会进行补偿操作。假如某一个锁无法获取,MultiLock
会释放已经获取的锁,并且返回失败,避免部分锁被获取时产生不一致的状态。
锁的自动释放
Redisson 的锁是基于 Redis 的 EXPIRE
命令设置过期时间的,默认情况下,MultiLock
在获取锁时会设置一个超时时间。如果某个操作在超时后未完成,锁会被自动释放。这样可以防止由于某个节点的崩溃或死锁而导致锁无法释放。
MultiLock
是 Redisson 中的一个组合锁,用于同时操作多个分布式锁。由于这些锁可能在不同时间点被获取,为了保证锁的整体一致性,需要在所有锁被获取后重新设置每个锁的过期时间,从而确保锁的 自动释放 功能能够正确生效。
关键代码
以下是设置每个子锁过期时间的核心逻辑:
leaseTime == -1的时候回触发看门狗机制,通过看门狗机制来管理锁的续约和释放。因此不需要在所有锁被获取后重新设置每个锁的过期时间。
if (leaseTime != -1) {
List<RFuture<Boolean>> futures = new ArrayList<>(acquiredLocks.size());
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = ((RedissonLock) rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture : futures) {
rFuture.syncUninterruptibly();
}
}
详细解释
1. 检查租约时间是否设置
if (leaseTime != -1)
- 目的:确定是否需要为锁设置租约时间。
- 逻辑:如果
leaseTime
不等于-1
,则表示需要为锁设置一个租约时间;否则,跳过这一部分,可能意味着锁会一直持有直到显式释放(或依赖看门狗机制)。
2. 初始化 futures
列表
List<RFuture<Boolean>> futures = new ArrayList<>(acquiredLocks.size());
- 目的:创建一个
List
来存储异步操作的结果(RFuture<Boolean>
)。 - 说明:
acquiredLocks
是已经成功获取的所有子锁的集合。列表的初始容量设置为acquiredLocks.size()
,以优化内存分配。
3. 为每个子锁设置租约时间(异步操作)
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = ((RedissonLock) rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
- 遍历所有已获取的子锁:对每一个
RLock
(具体为RedissonLock
实现)进行操作。 - 类型转换:将
RLock
转换为RedissonLock
,以访问expireAsync
方法。 - 调用
expireAsync
方法:- 功能:异步地为 Redis 锁设置过期时间(即租约时间)。
- 参数:
unit.toMillis(leaseTime)
:将租约时间转换为毫秒。TimeUnit.MILLISECONDS
:指定时间单位为毫秒。
- 返回值:
RFuture<Boolean>
,表示异步操作的结果,Boolean
值表示是否成功设置了过期时间。
- 添加到
futures
列表:将每个异步操作的RFuture
对象添加到futures
列表中,以便后续等待其完成。
4. 等待所有异步操作完成
for (RFuture<Boolean> rFuture : futures) { rFuture.syncUninterruptibly(); }
- 遍历所有
RFuture
对象:对futures
列表中的每一个RFuture
进行操作。 - 调用
syncUninterruptibly()
方法:- 功能:同步等待异步操作完成,且不会因为线程被中断而抛出
InterruptedException
。 - 结果处理:虽然在此代码片段中没有显式处理
Boolean
结果,但调用syncUninterruptibly()
确保所有租约时间设置操作已经完成,无论成功与否。
- 功能:同步等待异步操作完成,且不会因为线程被中断而抛出
- 目的:确保所有子锁的租约时间都已被设置,以维护锁的一致性和防止死锁。
为什么需要重新设置过期时间?
MultiLock
是一个组合锁,由多个子锁(RLock
)组成。由于这些子锁是在不同时间点逐步获取的,它们的过期时间可能不同。若不统一重置过期时间,会导致以下问题:
- 过期时间不一致:第一个获取的子锁会比最后一个获取的子锁更早过期,从而导致
MultiLock
整体锁的失效时间变短。 - 潜在锁丢失问题:如果某个子锁率先过期,Redis 会自动释放该锁,导致
MultiLock
的保护失效。
通过重新设置所有子锁的过期时间,能够统一锁的失效时间,确保所有子锁在 leaseTime
内保持有效。
总结
MultiLock
的原理主要基于多个 RLock
锁的组合。它的工作方式如下:
组合多个子锁:
MultiLock
是由多个RLock
(子锁)组成的。每个子锁对应一个独立的 Redis 锁。- 通过将多个子锁组合在一起,
MultiLock
能够实现跨多个 Redis 节点或多个资源的锁定操作。
加锁操作:
- 当调用
MultiLock.lock()
时,它会依次尝试获取所有子锁。 - 如果所有子锁都成功获取,锁定操作完成,持有
MultiLock
。 - 如果任何一个子锁获取失败,则回滚之前成功获取的所有锁,确保不会部分持有锁。
解锁操作:
- 当调用
MultiLock.unlock()
时,MultiLock
会依次释放所有子锁。 - 每个子锁的释放操作是独立的,但整体上保证所有子锁最终被释放。
租约时间设置:
- 如果指定了租约时间(
leaseTime
),MultiLock
会为每个子锁设置相同的超时时间。 - 由于子锁可能在不同时间点被获取,
MultiLock
会重新为每个子锁设置统一的过期时间,确保锁的整体一致性。 - 如果没有指定租约时间,会触发看门狗机制自动为锁续约
自动释放:
- 如果持有锁的客户端在租约时间内未释放锁,Redis 会根据
EXPIRE
删除子锁键,自动释放锁,防止死锁。
异步和并发优化:
MultiLock
使用异步方法(如expireAsync
)并行地为多个子锁设置过期时间,提高操作效率。- 在等待异步结果时,通过同步等待(
syncUninterruptibly
)确保所有操作完成。