在分布式系统中,为了确保业务操作的一致性和数据安全,我们常常需要对多个资源(如订单、库存、商品等)同时加锁。虽然 Redisson 提供的单一资源锁(RLock)使用简单,但在业务逻辑涉及多个资源时,仅靠单个锁显得力不从心。为此,Redisson 提供了**联锁(MultiLock)**机制,它能把多个 RLock 组合成一个整体锁,只有当所有子锁都成功加锁后,才能算真正拿到了锁。
1. RedissonMultiLock 的基本原理
RedissonMultiLock 的设计思路其实很简单,主要有以下几个特点:
- 整体加锁:只有当传入的所有 RLock 都成功加锁,才认为联锁获取成功;否则,已经拿到的子锁会被全部释放,并重新尝试加锁。
- 自动续期机制:即便设置了无限期持有(leaseTime 为 -1),Redisson 内部会启动看门狗机制自动延长锁的有效期,避免业务处理时间过长而导致锁被误释放。
- 失败策略:在加锁过程中,如果任何一个子锁获取失败(例如遇到 Redis 响应超时),所有已获取的子锁都会被释放,然后重试,直到在规定等待时间内全部获得锁。
- 底层实现 :联锁主要依赖于各个 RLock 的
tryLock(waitTime, leaseTime, unit)
方法,通过依次遍历各个锁来实现加锁,释放时依次调用每个锁的unlock()
方法。
适用场景
- 分布式订单处理:在处理订单时同时锁定订单、库存、商品等多个关键资源,确保在高并发环境下数据始终保持一致。
- 跨服务协同:多个服务(甚至分布在不同 Redisson 客户端)同时操作同一批资源时,联锁可以确保所有服务拿到锁后再开始操作。
- 复杂事务控制:当一个操作涉及多个子步骤,需要保证要么全部成功要么全部回滚时,使用联锁能够确保各个资源都处于受控状态。
2. 优化后的代码示例
下面的代码展示了如何在 Spring Boot 环境下使用 RedissonMultiLock。示例不仅说明了如何加锁、执行业务逻辑和安全释放锁,还对等待时间和租约时间的设置做了详细说明。
java
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.RedissonMultiLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* 示例展示了如何使用 RedissonMultiLock 将多个 RLock 组合成一个整体锁,
* 从而确保只有当所有子锁都成功加锁后才执行业务逻辑。
*
* 使用场景例如分布式订单系统中,同时锁定多个资源,确保操作的原子性。
*
* 注意:
* 1. 若任意一个子锁获取失败,会释放已获得的所有锁并重试;
* 2. 释放锁前,最好判断当前线程是否持有该锁,避免误解锁导致异常;
* 3. 当 leaseTime 为 -1 时,系统会自动续期锁的有效期(看门狗机制)。
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class RedissonMultiLockTest {
@Autowired
private RedissonClient redissonClient;
// 创建一个固定大小的线程池,用于并发测试(实际使用时可根据需要调整线程数)
private final ExecutorService executorService = Executors.newFixedThreadPool(3);
@After
public void tearDown() {
// 测试结束后关闭线程池,防止资源泄露
executorService.shutdown();
}
/**
* 测试 RedissonMultiLock 的基本用法:
* - 任务1尝试同时获取 lock1 和 lock2,成功拿到联锁后开始执行业务逻辑;
* - 任务2和任务3分别尝试获取 lock1 与 lock2,预期因联锁占用而拿不到锁。
*
* 说明:
* 1. 联锁加锁时的等待时间根据锁数量设置(此处为 locks.size() * 1500 毫秒左右);
* 2. 若 leaseTime 为 -1,则启用看门狗机制,自动续期防止锁超时。
*/
@SneakyThrows
@Test
public void testTryMultiLock() {
// 初始化单个锁
RLock lock1 = redissonClient.getLock("lock1");
RLock lock2 = redissonClient.getLock("lock2");
// 创建联锁:只有 lock1 和 lock2 都加锁成功才算成功
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);
// 任务1:尝试获取联锁
Runnable task1 = () -> {
boolean locked = false;
try {
// 参数说明:
// waitTime:最大等待时间(这里计算得出,如2个锁时大约3000~4500毫秒)
// leaseTime:租约时间,-1 表示启用看门狗自动续期
if (multiLock.tryLock(1L, 10L, TimeUnit.SECONDS)) {
locked = true;
log.info("【任务1】成功获取联锁,开始执行业务逻辑...");
// 模拟业务处理
Thread.sleep(2000);
log.info("【任务1】业务处理完毕。");
} else {
log.warn("【任务1】获取联锁失败。");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("【任务1】执行过程中被中断", e);
} finally {
if (locked) {
// 释放联锁(会依次释放 lock1 和 lock2)
multiLock.unlock();
log.info("【任务1】联锁已释放。");
}
}
};
// 任务2:尝试获取 lock1(由于联锁中已锁定 lock1,预计获取失败)
Runnable task2 = () -> {
boolean locked = false;
RLock lock = redissonClient.getLock("lock1");
try {
if (lock.tryLock(0L, 5L, TimeUnit.SECONDS)) {
locked = true;
log.info("【任务2】成功获取 lock1,开始执行业务逻辑...");
Thread.sleep(2000);
log.info("【任务2】业务处理完毕。");
} else {
log.warn("【任务2】获取 lock1 失败。");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("【任务2】执行过程中被中断", e);
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("【任务2】lock1 已释放。");
}
}
};
// 任务3:尝试获取 lock2(同上,预计获取失败)
Runnable task3 = () -> {
boolean locked = false;
RLock lock = redissonClient.getLock("lock2");
try {
if (lock.tryLock(0L, 5L, TimeUnit.SECONDS)) {
locked = true;
log.info("【任务3】成功获取 lock2,开始执行业务逻辑...");
Thread.sleep(2000);
log.info("【任务3】业务处理完毕。");
} else {
log.warn("【任务3】获取 lock2 失败。");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("【任务3】执行过程中被中断", e);
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("【任务3】lock2 已释放。");
}
}
};
// 先提交任务1以确保先拿到联锁,再依次提交任务2和任务3
Future<?> future1 = executorService.submit(task1);
Thread.sleep(20);
Future<?> future2 = executorService.submit(task2);
Future<?> future3 = executorService.submit(task3);
// 等待所有任务结束
future1.get();
future2.get();
future3.get();
}
/**
* 简单测试:确保异步任务能正常执行,避免主线程提前结束。
*/
@SneakyThrows
@Test
public void testSimpleExecution() {
Runnable task = () -> {
try {
log.info("【任务】开始执行...");
Thread.sleep(2000);
log.info("【任务】执行结束。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("【任务】执行过程中被中断", e);
}
};
Future<?> future = executorService.submit(task);
future.get();
}
}
3. 总结
- RedissonMultiLock 可以帮助我们同时锁定多个关键资源,只有所有子锁都成功后才执行后续操作,保证数据一致性。
- 建议 :
- 调用加锁方法前,根据实际情况设置合适的等待时间和租约时间,充分利用看门狗机制防止锁误释放;
- 释放锁时最好判断当前线程是否持有该锁,以避免误解锁导致异常;
- 应用场景:适用于分布式订单处理、跨服务协同操作和复杂事务控制等需要同时操作多个资源的场景。