深入解析 RedissonMultiLock —— 分布式联锁的原理与实战

在分布式系统中,为了确保业务操作的一致性和数据安全,我们常常需要对多个资源(如订单、库存、商品等)同时加锁。虽然 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 可以帮助我们同时锁定多个关键资源,只有所有子锁都成功后才执行后续操作,保证数据一致性。
  • 建议
    • 调用加锁方法前,根据实际情况设置合适的等待时间和租约时间,充分利用看门狗机制防止锁误释放;
    • 释放锁时最好判断当前线程是否持有该锁,以避免误解锁导致异常;
  • 应用场景:适用于分布式订单处理、跨服务协同操作和复杂事务控制等需要同时操作多个资源的场景。
相关推荐
Hole_up1 小时前
【hadoop】远程调试环境
大数据·hadoop·分布式
小样vvv2 小时前
【Kafka】分布式消息队列的核心奥秘
分布式·kafka
晴天Y283 小时前
redis部署架构
数据库·redis·架构
蓝色之鹰4 小时前
RabbitMQ经典面试题及答案
分布式·rabbitmq
Foyo Designer4 小时前
【 <二> 丹方改良:Spring 时代的 JavaWeb】之 Spring Boot 中的缓存技术:使用 Redis 提升性能
java·spring boot·redis·spring·缓存
Lansonli4 小时前
大数据Spark(五十五):Spark框架及特点
大数据·分布式·spark
JIU_WW5 小时前
Redis大key问题
数据库·redis
大桶矿泉水5 小时前
qt之使用redis与其他程序(python)交互同通信
数据库·redis·缓存·银河麒麟redis·linux redis
不懂的浪漫7 小时前
夯实 kafka 系列|第五章:基于 kafka 分布式事件框架 eval-event
分布式·kafka
kill bert8 小时前
第30周Java分布式入门 docker
java·分布式·docker