深入解析 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 可以帮助我们同时锁定多个关键资源,只有所有子锁都成功后才执行后续操作,保证数据一致性。
  • 建议
    • 调用加锁方法前,根据实际情况设置合适的等待时间和租约时间,充分利用看门狗机制防止锁误释放;
    • 释放锁时最好判断当前线程是否持有该锁,以避免误解锁导致异常;
  • 应用场景:适用于分布式订单处理、跨服务协同操作和复杂事务控制等需要同时操作多个资源的场景。
相关推荐
Juicedata1 小时前
分布式架构下配额设计:JuiceFS 的实现与典型案例
分布式·架构
NCIN EXPE7 小时前
redis 使用
数据库·redis·缓存
hERS EOUS7 小时前
nginx 代理 redis
运维·redis·nginx
NoSi EFUL9 小时前
redis存取list集合
windows·redis·list
Deepincode9 小时前
Redis源码探究系列—SDS 扩容策略与内存预分配机制
redis
程序员老邢10 小时前
【技术底稿 19】Redis7 集群密码配置 + 权限锁死 + 磁盘占满连锁故障真实排查全记录
java·服务器·经验分享·redis·程序人生·微服务
coNh OOSI12 小时前
Redis——Windows安装
数据库·windows·redis
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.12 小时前
Redis主从复制配置全攻略
数据库·redis·笔记
csdn2015_12 小时前
修改分类信息的时候将分类异步写入redis
数据库·redis·bootstrap