Redisson 分布式锁使用教程

大纲

前言

学习资源

Redisson 简介

Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid)。充分地利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中的常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。Redisson 的宗旨是促进使用者对 Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。值得一提的是,Redisson 底层采用的是 Netty 框架。支持 Redis 2.8 以上版本,支持 Java 1.6+ 以上版本。

Redisson 对象

Redis 命令和 Redisson 对象匹配列表请阅读 这里

Redisson 基础使用

Spring 整合 Redisson

  • 引入 Maven 依赖
1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.0</version>
</dependency>
  • 配置 Redisson 客户端的连接信息,包括 Reids 服务器的地址、密码等内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class RedisssonConfig {

@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() throws IOException {
Config config = new Config();
config.useSingleServer()
// 地址
.setAddress("redis://127.0.0.1:6379")
// 密码
.setPassword("123456");
return Redisson.create(config);
}

}

提示

  • 上述的配置方式同样适用于 SpringBoot 项目。
  • 配置完 Redisson 客户端后,在 Java 业务代码里就可以直接注入 RedissonClient 实例对象来使用 Redisson 提供的各种分布式锁了。

可重入锁 (Reentrant Lock)

基于 Redis 的 Redisson 分布式可重入锁 RLock 实现了 java.util.concurrent.locks.Lock 接口,同时还提供了异步(Async)、反射式(Reactive)和 RxJava2 标准的接口。众所周知,如果负责储存这个分布式锁的 Redisson 节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson 内部提供了一个监控锁的看门狗,它的作用是在 Redisson 实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是 30 秒钟,也可以通过修改 Config.lockWatchdogTimeout 来另行指定。另外 Redisson 还为加锁的方法提供了 leaseTime 参数来指定加锁的时间,超过这个时间后锁便会自动解开。

  • 直接获取锁,阻塞等待直至获取到锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@SpringBootTest
public class RedissonTest {

@Autowired
private RedissonClient redissonClient;

@Test
public void rLock() throws InterruptedException {
// 获取可重入锁
RLock lock = redissonClient.getLock("rLock");

// 阻塞等待,直至获取到锁
lock.lock();
try {
System.out.println("==> success to get locker");
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}

}

提示

  • RLock.lock() 方法加锁后,默认加的锁的有效期是 30 秒。
  • RLock.lock() 方法加锁后,如果业务耗时超长,Redisson 在业务执行期间会周期性地自动给锁续上新的 30 秒有效期(看门狗机制),不用担心业务执行时间过长,锁自动过期被删掉的问题。
  • RLock.lock() 方法加锁后,只要加锁的业务运行完成,Redisson 就不会再给当前锁续期,即使不手动解锁,锁默认会在 30 秒内自动删除。
  • 直接获取锁,阻塞等待直至获取到锁,且上锁以后 10 秒自动解锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@SpringBootTest
public class RedissonTest {

@Autowired
private RedissonClient redissonClient;

@Test
public void rLock() throws InterruptedException {
// 获取可重入锁
RLock lock = redissonClient.getLock("rLock");

// 阻塞等待,直至获取到锁,且上锁以后10秒自动解锁
lock.lock(10, TimeUnit.SECONDS);
try {
System.out.println("==> success to get locker");
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}

}

特别注意

  • 调用 RLock.lock(10, TimeUnit.SECONDS) 方法加锁时,设置自动解锁的时间必须大于业务的执行时间。
  • 调用 RLock.lock(10, TimeUnit.SECONDS) 方法加锁时,在锁时间到了以后,即使业务未执行完成,Redisson 也不会给锁续期,也就是看门狗机制此时不会生效。
  • 尝试获取锁,阻塞等待,但不能超过指定的最大等待时间,且上锁以后 10 秒自动解锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@SpringBootTest
public class RedissonTest {

@Autowired
private RedissonClient redissonClient;

@Test
public void rLock() throws InterruptedException {
// 获取可重入锁
RLock lock = redissonClient.getLock("rLock");

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
System.out.println("==> success to get locker");
Thread.sleep(5000);
} finally {
// 解锁
lock.unlock();
}
}
}

}

特别注意

  • 调用 RLock.tryLock(100, 10, TimeUnit.SECONDS) 方法加锁时,设置自动解锁的时间必须大于业务的执行时间。
  • 调用 RLock.tryLock(100, 10, TimeUnit.SECONDS) 方法加锁时,在锁时间到了以后,即使业务未执行完成,Redisson 也不会给锁续期,也就是看门狗机制此时不会生效。

读写锁 (ReadWriteLock)

基于 Redis 的 Redisson 分布式可重入读写锁 RReadWriteLock 实现了 java.util.concurrent.locks.ReadWriteLock 接口,其中读锁和写锁都继承了 RLock 接口。分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

读写锁的特性

  • 读 + 读:相当于无锁,支持并发读
  • 写 + 读:读操作需要等待写操作完成
  • 读 + 写:写操作需要等待读操作完成
  • 写 + 写:互斥,需要等待对方的锁释放
  • 简而言之,只要有写锁存在,则其他操作都必须阻塞等待
  • 单元测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@SpringBootTest
public class RedissonTest {

@Autowired
private RedissonClient redissonClient;

/**
* 缓存(非线程安全)
*/
public static final Map<String, String> CACHES = new HashMap<>();

/**
* 写入数据
*/
private String writeValue() {
// 获取写锁
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rw-lock");
RLock writeLock = rwLock.writeLock();

String uuid = UUID.randomUUID().toString();
try {
// 加写锁
writeLock.lock();
Thread.sleep(8000);
CACHES.put("uuid", uuid);
System.out.println("==> write uuid : " + uuid);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 解写锁
writeLock.unlock();
}
return uuid;
}

/**
* 读取数据
*/
private String readValue() {
// 获取读锁
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rw-lock");
RLock readLock = rwLock.readLock();

String uuid = null;
try {
// 加读锁
readLock.lock();
uuid = CACHES.get("uuid");
System.out.println("==> read uuid : " + uuid);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 解读锁
readLock.unlock();
}
return uuid;
}

@Test
public void readWriteLock() throws Exception {
// 写操作
new Thread(this::writeValue).start();

Thread.sleep(500);

// 读操作(会阻塞等待写操作完成才执行)
new Thread(this::readValue).start();

System.in.read();
}

}
  • 单元测试结果
1
2
==> write uuid : 7d611f3a-2437-413d-b1aa-4041decc344e
==> read uuid : 7d611f3a-2437-413d-b1aa-4041decc344e

提示

  • 读锁是一个共享锁,支持并发地执行读操作。
  • 写锁是一个排他锁(互斥锁 / 独占锁),可防止并发地执行写操作。
  • 使用读写锁,可以保证读到的数据永远是最新的;只要写锁没有释放掉,那么拥有读锁的操作就会一直阻塞等待,直至写锁被释放。

闭锁 (CountDownLatch)

基于 Redis 的 Redisson 分布式闭锁 RCountDownLatch 采用了与 java.util.concurrent.CountDownLatch 相似的接口和用法。闭锁适用于等待一个多线程的操作,也就是等待 N 个线程把所有业务执行完毕后,再处理一个业务。关于闭锁的使用场景,可以想象一下公司的门卫如何等所有员工下班后再关门。公司一共有五名员工,门卫需要等这五名员工下班后,才能关闭大门。

闭锁的使用场景

  • 闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:
  • (a) 确保某个计算在其需要的所有资源都被初始化之后才继续执行
  • (b) 确保某个服务在其他依赖的所有其他服务都已经启动之后才启动
  • (c) 等待直到某个操作所有参与者都准备就绪再继续执行
  • 单元测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@SpringBootTest
public class RedissonTest {

@Autowired
private RedissonClient redissonClient;

/**
* 门卫关门
*/
public void lockDoor() {
RCountDownLatch countDownLatch = redissonClient.getCountDownLatch("countDownLatch");
// 设置总共有5个员工
countDownLatch.trySetCount(5);

try {
// 门卫等待所有员工下班
countDownLatch.await();
System.out.println("==> 门卫关门成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

/**
* 员工下班
*/
public void offWork(long num) {
RCountDownLatch countDownLatch = redissonClient.getCountDownLatch("countDownLatch");
// 未下班的员工计数减一
countDownLatch.countDown();
System.out.println("==> " + num + " 号员工下班");
}

@Test
public void countDownLatch() throws Exception {
// 模拟门卫关门
new Thread(this::lockDoor).start();

Thread.sleep(1000);

// 模拟5个员工下班
for (int i = 0; i < 5; i++) {
new Thread(() -> {
offWork(Thread.currentThread().getId());
}).start();
}

System.in.read();
}

}
  • 单元测试结果
1
2
3
4
5
6
==> 113 号员工下班
==> 112 号员工下班
==> 114 号员工下班
==> 115 号员工下班
==> 116 号员工下班
==> 门卫关门成功

信号量 (Semaphore)

基于 Redis 的 Redisson 的分布式信号量 RSemaphore 采用了与 java.util.concurrent.Semaphore 相似的接口和用法,同时还提供了异步(Async)、反射式(Reactive)和 RxJava2 标准的接口。关于信号量的使用场景,可以想象一下平时停车场如何停车。一共有十辆车准备停车,停车位有五个,当五个停车位满了后,其他车只能等有车位空出来才能停车。可以把停车位比作信号,现在有五个信号,停一次车,用掉一个信号,车离开就是释放一个信号。值得一提的是,RSemaphore 可用于实现分布式限流。RSemaphore 的原理图如下。

  • 单元测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@SpringBootTest
public class RedissonTest {

@Autowired
private RedissonClient redissonClient;

@Test
public void semaphore() throws IOException {
// 获取信号量
RSemaphore semaphore = redissonClient.getSemaphore("semaphore");

// 设置许可数量,模拟五个停车位
semaphore.trySetPermits(5);

// 创建10个线程,模拟10辆车过来停车
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
try {
// 占用信号(停车位)
semaphore.acquire();
Thread.sleep(1000);
System.out.println("==> 车辆 " + Thread.currentThread().getId() + " 进入停车场");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放信号(停车位)
semaphore.release();
System.out.println("==> 车辆 " + Thread.currentThread().getId() + " 离开停车场");
}
});
}
System.in.read();
}

}
  • 单元测试结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
==> 车辆 113 进入停车场
==> 车辆 110 进入停车场
==> 车辆 109 进入停车场
==> 车辆 111 进入停车场
==> 车辆 108 进入停车场
==> 车辆 108 离开停车场
==> 车辆 109 离开停车场
==> 车辆 110 离开停车场
==> 车辆 111 离开停车场
==> 车辆 113 离开停车场
==> 车辆 112 进入停车场
==> 车辆 117 进入停车场
==> 车辆 114 进入停车场
==> 车辆 116 进入停车场
==> 车辆 116 离开停车场
==> 车辆 114 离开停车场
==> 车辆 112 离开停车场
==> 车辆 117 离开停车场
==> 车辆 115 进入停车场
==> 车辆 115 离开停车场

可过期性信号量 (PermitExpirableSemaphore)

基于 Redis 的 Redisson 可过期性信号量 RPermitExpirableSemaphore 是在 RSemaphore 对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的 ID 来辨识,释放时只能通过提交这个 ID 才能释放。它提供了异步(Async)、反射式(Reactive)和 RxJava2 标准的接口。

  • 单元测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@SpringBootTest
public class RedissonTest {

@Autowired
private RedissonClient redissonClient;

@Test
public void expirableSemaphore() throws IOException {
// 获取可过期性信号量
RPermitExpirableSemaphore semaphore = redissonClient.getPermitExpirableSemaphore("expirable-semaphore");

// 设置许可数量,模拟五个停车位
semaphore.trySetPermits(5);

// 创建10个线程,模拟10辆车过来停车
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 信号的 ID 标识
String permitId = null;
try {
// 占用信号量(停车位),有效期只有5秒
permitId = semaphore.acquire(5, TimeUnit.SECONDS);
Thread.sleep(1000);
System.out.println("==> 车辆 " + Thread.currentThread().getId() + " 进入停车场");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放信号量(停车位)
semaphore.release(permitId);
System.out.println("==> 车辆 " + Thread.currentThread().getId() + " 离开停车场");
}
}).start();
}
System.in.read();
}

}
  • 单元测试结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
==> 车辆 115 进入停车场
==> 车辆 109 进入停车场
==> 车辆 112 进入停车场
==> 车辆 111 进入停车场
==> 车辆 113 进入停车场
==> 车辆 113 离开停车场
==> 车辆 115 离开停车场
==> 车辆 111 离开停车场
==> 车辆 109 离开停车场
==> 车辆 112 离开停车场
==> 车辆 110 进入停车场
==> 车辆 114 进入停车场
==> 车辆 108 进入停车场
==> 车辆 106 进入停车场
==> 车辆 114 离开停车场
==> 车辆 108 离开停车场
==> 车辆 106 离开停车场
==> 车辆 110 离开停车场
==> 车辆 107 进入停车场
==> 车辆 107 离开停车场

Redisson 进阶使用

基于 RedLock 算法实现分布式锁

RedLock 算法的核心思想

RedLock 算法的核心思想是在多个独立的 Redis 实例(不存在主从、集群关系,彼此互相隔离)上同时获取锁,从而保证分布式锁的高可用性和可靠性。即使某些 Redis 实例出现故障,只要在大多数实例(至少 N/2 + 1 个)中成功获取到锁,整个系统仍然可以认为锁是成功获取的(有效)。

RedLock 算法的实现步骤

  • (1) 获取当前时间

    • 客户端获取当前时间,用于计算获取锁的总耗时。
  • (2) 依次向 N 个实例请求锁

    • 客户端依次向 N 个 Redis 实例(通常是 5 个)发送获取锁的请求。
    • 请求锁的命令是 SET key value NX PX milliseconds,其中:
      • key 是锁的标识。
      • value 是客户端的唯一标识(通常使用类似 UUID 这样的随机值)。
      • NX 表示只有在键不存在时才进行设置。
      • PX 表示设置键的过期时间(毫秒)。
  • (3) 计算总耗时

    • 客户端在每次请求锁后,立即获取当前时间,计算从开始请求到成功获取锁的总耗时。
  • (4) 验证锁获取成功

    • 如果客户端在大多数(至少 N/2 + 1 个)实例上成功获取锁,并且获取锁的总耗时小于锁的过期时间,则认为锁获取成功。
    • 如果锁获取失败,客户端应当立即释放在所有实例上已成功获取的锁。
  • (5) 使用锁

    • 客户端在获取锁成功后,执行相关的业务逻辑。
    • 为了防止锁提前被释放掉,业务逻辑应在锁的有效期内完成。
  • (6) 释放锁

    • 客户端在所有实例上执行释放锁操作,确保释放所有持有的锁。
    • 释放锁的操作通常是一个 Lua 脚本,用于原子性检查锁的值是否匹配客户端的唯一标识(防止误删其他客户端加的锁),然后再删除锁。

RedLock 算法的优缺点

  • 优点
    • 高可用性:通过在多个独立的 Redis 实例(没有主从、集群关系)上获取锁,RedLock 提高了锁的可用性和容错能力。
    • 强一致性:RedLock 保证了分布式锁的一致性,只要大多数实例成功获取锁,即使部分实例发生故障,仍然可以正确获取和释放锁。
    • 防止死锁:锁设置了过期时间,确保即使客户端崩溃或未能正常释放锁,锁也会在超时后自动释放,避免死锁问题。
  • 缺点
    • 实例独立性:要求每个 Redis 实例是独立且不会同时宕机,通常部署在不同的物理或虚拟机上。
    • 时钟同步:客户端的系统时钟需要尽可能准确,以确保计算获取锁的总耗时和锁的过期时间准确无误。
    • 网络延迟:在获取锁和释放锁的过程中,可能会受到网络延迟的影响,需要在系统设计时考虑这些因素。

RedLock 算法的部署建议

  • Redis 官方建议针对 RedLock 算法至少部署 5 个相互独立的 Redis 实例(不存在主从、集群关系,彼此互相隔离),以容忍 2 个实例出现故障;若服务器资源不足,要求部署至少 3 个相互独立的 Redis 实例,以容忍 1 个实例出现故障。

通过 Redisson 实现不同类型的 Redis 分布式锁

Redisson 基于 Redis 提供了多种分布式锁 API,包括:RLockRedissonMultiLockRedissonRedLock 等,如下图所示:

  • RLock(可重入锁)

    • 可以在单个 Redis 节点上加锁,也可在 Redis 集群的单个主节点上加锁。
    • 局限性:存在单机故障问题,或者集群模式下如果主节点故障且锁数据未同步到从节点,主从切换完成后,新主节点可能重复授予锁,从而导致锁的互斥性失效。
    • 主要实现类:
      • RedissonLock — 基于单个 Redis 节点(或 Redis 集群单个主节点)的普通可重入锁实现。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        // 获取可重入锁(基于 RLock 接口)
        RLock lock = redissonClient.getLock("my_lock");
        try {
        // 抢占锁
        lock.lock();
        // 执行业务逻辑
        } finally {
        // 释放锁
        lock.unlock();
        }
      • RedissonFairLock — 公平锁,按请求顺序获取锁,避免 “插队”。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        // 获取公平锁(基于 RLock 接口)
        RLock lock = redissonClient.getFairLock("my_lock");
        try {
        // 抢占锁
        lock.lock();
        // 执行业务逻辑
        } finally {
        // 释放锁
        lock.unlock();
        }
      • RedissonReadLock — 读写锁的读锁部分,支持读并发。
      • RedissonWriteLock — 读写锁的写锁部分,互斥访问。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        // 获取读写锁
        RReadWriteLock rwLock = redissonClient.getReadWriteLock("my_lock");

        // 获取读锁(基于 RLock 接口)
        RLock readLock = rwLock.readLock();
        try {
        // 抢占读锁
        readLock.lock();
        // 执行读操作
        } finally {
        // 释放读锁
        readLock.unlock();
        }

        // 获取写锁(基于 RLock 接口)
        RLock writeLock = rwLock.writeLock();
        try {
        // 抢占写锁
        writeLock.lock();
        // 执行写操作
        } finally {
        // 释放写锁
        writeLock.unlock();
        }
      • RedissonSpinLock — 自旋锁,短时间内高频尝试获取锁,减少阻塞等待。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        // 获取自旋锁(基于 RLock 接口)
        RLock spinLock = redisson.getSpinLock("my_lock");

        // 尝试加锁
        boolean locked = spinLock.tryLock(5000, 10000, TimeUnit.MILLISECONDS);
        if (locked) {
        try {
        // 执行业务逻辑
        } finally {
        // 释放锁
        spinLock.unlock();
        }
        }
  • ‌RedissonMultiLock(联锁)

    • 作用:一次性对多个独立的 RLock 加锁,所有锁都成功获取才算整体加锁成功。
    • 锁资源类型:这些锁可以是同一个 Redis 节点上不同的 Key,也可以是不同 Redis 节点上的同一个 Key。
    • 设计目的:侧重于跨资源的原子性加锁,而非 Redis 集群的容错与一致性保证。
    • 应用场景:适用于需要同时锁定多个资源(如订单、库存等)的跨资源并发控制。
    • 局限性:不符合 Redis 官方提出 RedLock 算法规范,在 Redis 集群场景下无法保证分布式锁的强一致性与安全性。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // 获取所有需要加锁的资源
      RLock lock1 = redissonClient.getLock("lock1");
      RLock lock2 = redissonClient.getLock("lock2");

      // 创建联锁
      RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);

      boolean locked = false;
      try {
      // 尝试获取多个锁资源的联锁,最多等待 5 秒,成功加锁后 10 秒自动释放,只有所有子锁都加锁成功才算获得锁
      locked = multiLock.tryLock(5, 10, TimeUnit.SECONDS);
      if (locked) {
      // 执行业务逻辑
      }
      } finally {
      // 只在获取锁成功时解锁
      if (locked) {
      multiLock.unlock();
      }
      }
  • RedissonRedLock(红锁)

    • 基于 Redis 官方的 RedLock 算法实现。
    • 在多个独立 Redis 节点上(不存在主从、集群关系,彼此互相隔离)尝试加锁;
    • 只有当超过半数(N/2 + 1)的节点加锁成功时,才认为锁获取成功,符合 RedLock 的多数派原则;
    • 优势:严格实现 Redis 官方提出的 RedLock 算法。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      // 直连第一个独立 Redis 节点
      Config config1 = new Config();
      config1.useSingleServer().setAddress("redis://127.0.0.1:6379");
      RedissonClient redissonClient1 = Redisson.create(config1);

      // 直连第二个独立 Redis 节点
      Config config2 = new Config();
      config2.useSingleServer().setAddress("redis://127.0.0.1:6380");
      RedissonClient redissonClient2 = Redisson.create(config2);

      // 直连第三个独立 Redis 节点
      Config config3 = new Config();
      config3.useSingleServer().setAddress("redis://127.0.0.1:6381");
      RedissonClient redissonClient3 = Redisson.create(config3);

      // 在每个 Redis 节点上获取一个普通 RLock
      RLock lock1 = redissonClient1.getLock("lock1");
      RLock lock2 = redissonClient2.getLock("lock2");
      RLock lock3 = redissonClient3.getLock("lock3");

      // 组合成一个 RedLock
      RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

      boolean isLocked = false;
      try {
      // 尝试同时加锁(要求在大部分节点上加锁成功才算真正获得锁),等待最多 100 秒,加锁成功后 10 秒自动释放
      isLocked = redLock.tryLock(100, 10, TimeUnit.SECONDS);
      if (isLocked) {
      // 执行业务逻辑
      }
      } finally {
      // 只在获取锁成功时解锁
      if (isLocked) {
      redLock.unlock();
      }
      }

      // 关闭连接
      redissonClient1.shutdown();
      redissonClient2.shutdown();
      redissonClient3.shutdown();

RedissonRedLock 的正确使用方法

  • RedLock 算法的核心思想是在多个独立的 Redis 实例(不存在主从、集群关系,彼此互相隔离)上同时获取锁,从而保证分布式锁的高可用性和可靠性。即使某些 Redis 实例出现故障,只要在大多数实例(至少 N/2 + 1 个)中成功获取到锁,整个系统仍然可以认为锁是成功获取的(有效)。
  • 因此,在使用 RedissonRedLock 时,强烈建议直接部署多个独立的 Redis 实例(不存在主从、集群关系,彼此互相隔离),或者多个独立 Redis 集群的 Master 节点,通常节点数量为奇数(比如 3 个或者 5 个),然后分别连接这些 Redis 节点,获取各自的 RLock,再组合成 RedissonRedLock
  • 使用 RedissonRedLock 时,千万不要依赖同一个 Redis 集群的多个 Maser 节点作为 RedLock 节点;因为同一个 Redis 集群的 Master 节点不是独立节点,由于使用分片存储方式,相同的 Key 永远只会落到同一个 Master 节点,因此不能满足 RedLock 的多数派原则,容易破坏分布式锁的强一致性。另一个更重要的原因是,集群内部发生故障转移(主从切换)期间可能会造成锁丢失。

在单个 Redis 集群中使用 RedissonRedLock 的问题

不建议在单个 Redis 集群中使用 RedissonRedLock 来保证强一致性和安全性,因为它无法达到设计目标,甚至可能比使用单个 Redis 节点或者 Redis 集群自身的锁机制(Redisson 的 RLock)更不安全。原因如下:

  • (1) RedLock 算法的核心前提是独立故障域:

    • RedLock 的设计初衷是用于多个独立 Redis 节点的环境(例如,部署在不同物理机、不同机架、甚至不同数据中心的 3 个或 5 个单独的 Redis 实例,没有主从关系)。
    • 它的安全性依赖于一个关键假设:这些节点是故障隔离的。一个节点失效(宕机、网络分区)不会立即或必然导致另一个节点也失效。
  • (2) Redis 集群破坏了 “独立故障域” 的前提:

    • 比如,在 3 主 3 从 的 Redis 集群中:
      • 主节点之间是协作关系: 它们共同组成一个逻辑集群,通过 Gossip 协议通信共享集群状态。
      • 主从节点是强关联的: 每个主节点都有对应的从节点。当主节点故障时,集群会自动进行故障转移,将其一个从节点提升为新的主节点,这个提升过程是自动且快速的。
    • 关键问题 - 故障转移期间的锁丢失(这是 RedLock 在集群环境下失效的核心原因)
      • 假设客户端 C1 成功使用 RedLock 在 3 个主节点(M1、M2、M3)上都获取了锁(满足 RedLock 的 N/2 + 1 原则)。
      • 在 C1 持有锁期间,主节点 M1 发生故障。
      • 集群检测到 M1 故障,触发故障转移。M1 的一个从节点(S1)被提升为新的主节点(称为 NewM1)。
      • 问题在于:故障转移过程(特别是异步复制)可能导致 NewM1 上缺失 C1 在旧 M1 上设置的锁!
        • 如果 C1 在旧 M1 上设置的锁信息还没来得及复制到 S1,那么 NewM1 启动后就没有这个锁的记录。
        • 即使旧 M1 上设置的锁复制到 NewM1 里面了,故障转移过程也可能导致短暂的数据不一致窗口。
      • 此时,另一个客户端 C2 尝试获取同一把锁。它使用 RedLock 向当前存活的主节点(NewM1、M2、M3)发起加锁请求。
      • C2 很可能在 NewM1(它上面没有旧锁记录)和另外两个节点中的至少一个(比如 M2)上成功获取锁,从而满足 RedLock 的 N/2 + 1 原则(例如 2/3)。这样,C1 和 C2 同时认为自己持有锁,导致分布式锁的互斥性被破坏!
  • (3) 集群网络分区(脑裂)问题加剧风险:

    • 如果发生网络分区,将集群分割成两个或多个无法通信的小分区。
    • 每个小分区可能都认为自己拥有足够的主节点(满足 RedLock 的 N/2 + 1 原则),并允许客户端在不同分区内获取同一把锁,这种情况同样导致多个客户端同时获取到锁,导致分布式锁的互斥性被破坏。
  • (4) 性能开销无意义:

    • RedLock 需要在多个独立的 Redis 节点上顺序执行加锁操作,这本身就有显著性能开销。
    • 在一个 Redis 集群内部使用 RedLock,这些操作本质上是跨节点通信,开销比在单个节点上加锁大很多,但却没有换来预期的安全性提升,得不偿失。

Redis 集群自身的锁机制(Redisson 的 RLock)对比在单个 Redis 集群中使用 RedissonRedLock

  • Redisson 的普通 RLock 对象(redissonClient.getLock("myLock"))在 Redis 集群模式下是安全可用的。
  • 它的工作原理:
    • (1) 客户端根据 Key 计算哈希槽。
    • (2) 将加锁请求发送到负责存储该哈希槽的主节点。
    • (3) 锁只存储在这个单个主节点上。
    • (4) 主节点会异步复制锁信息(SET 命令)给它的从节点。
  • 优点:简单、高效,利用了集群的分片和故障转移能力。
  • 缺点:在极端故障场景下(主节点宕机且其锁信息未能复制到新提升的主节点),也存在短暂的锁丢失风险(与 RedLock 在集群下遇到的问题是类似的本质,但只发生在一个分片上)。然而,这种风险通常被认为是可接受的,并且比在单个 Redis 集群内错误使用 RedLock 导致必然的互斥性破坏要小得多、可控得多。
  • 关键区别: Redis 集群模式下的 RLock 只依赖一个主节点,而在单个集群内使用的 RedLock 需要依赖多个主节点。但是,在单个 Redis 集群环境下,这多个主节点不是独立的故障域,故障转移机制使得依赖多个节点反而引入了额外的、更严重的失效点(即新主节点丢失旧锁)。

RedLock 算法的使用总结与建议:

  • 绝对要避免在单个 Redis 集群内使用 RedissonRedLock:它无法提供比 Redis 集群自身的锁机制(Redisson 的 RLock)更好的安全性,反而会因为集群的自动故障转移特性,在高可用设计下引入更严重的锁互斥性破坏风险。
  • 在单个 Redis 集群中,使用 Redisson 的普通 RLock(集群模式): 这是正确且推荐的方式。它利用了 Redis 集群的分片和故障转移特性,提供了在集群环境下合理的高可用分布式锁。但需要理解并接受其在极端故障场景下(主节点崩溃 + 锁未被复制)存在理论上的短暂锁失效风险,但在实践中,结合合理的锁超时设置,这通常是可管理的风险。
  • RedLock 的正确使用场景:仅当拥有 N 个 (N 为奇数,通常 3 或 5) 完全独立部署、不存在主从关系、且物理 / 逻辑故障域隔离 的 Redis 节点 / 实例时,才应该考虑使用 RedLock。例如:
    • 3 个分别部署在独立虚拟机或物理机上的 Redis 单机实例。
    • 3 个独立的 Redis Sentinel(比如,每个 Sentinel 都是一主多从,且都有自己的 Redis 主节点)。
    • 3 个独立的 Redis Cluster(每个集群作为一个逻辑节点参与 RedLock,但这很复杂且通常不推荐,不如直接用独立实例)。

SpringBoot 整合 Redisson

引入 Maven 依赖

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.19.0</version>
</dependency>

添加 YML 配置信息

  • 配置 Redis 的连接信息,包括主机地址、端口、密码等信息。
1
2
3
4
5
6
7
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 0
timeout: 5000

创建 Redission 配置类

  • 创建 Redission 配置类,用于定义 Redission 的客户端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedisssonConfig {

@Autowired
private RedisProperties redisProperties;

@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
String password = redisProperties.getPassword();
String url = String.format("redis://%s:%s", redisProperties.getHost() + "", redisProperties.getPort() + "");

Config config = new Config();
config.useSingleServer().setAddress(url).setPassword(password);
return Redisson.create(config);
}

}

单元测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@SpringBootTest
public class RedissonTest {

@Autowired
private RedissonClient redissonClient;

@Test
public void rLock() throws InterruptedException {
// 获取可重入锁
RLock lock = redissonClient.getLock("rLock");

// 阻塞等待,直至获取到锁
lock.lock();
try {
System.out.println("==> success to get locker");
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}

}

参考博客