【DB系列】redisson分布式锁使用及注意事项

文章目录
  1. I. 项目环境
    1. 1. pom依赖
    2. 2. 配置文件
  2. II. 分布式锁
    1. 1. 使用姿势
    2. 2. 锁无法释放场景
  3. II. 其他
    1. 0. 项目
    2. 1. 一灰灰Blog

redis使用分布式锁,除了我们自己借助setnx来实现之外,更为推荐的是借助redisson来完成,借助redisson,可以非常方便的使用redis分布锁,但是一个使用姿势不对,将可能导致锁无法释放问题

本文将介绍一下SpringBoot中redisson分布式锁的使用姿势,以及使用不当导致锁无法释放的演示

I. 项目环境

1. pom依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA + redis进行开发

下面是核心的pom.xml(源码可以再文末获取)

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.0</version>
</dependency>
</dependencies>

2. 配置文件

redis的配置,我们这里采用默认的配置,本机启动一个redis实例

1
2
3
4
5
spring:
redis:
host: 127.0.0.1
port: 6379
password:

II. 分布式锁

1. 使用姿势

核心类就是获取一个RedissonClient实例,然后借助它来获取锁

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
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private String port;

@Value("${spring.redis.password:}")
private String password;

@Bean
public RedissonClient redissonClient() {

Config config = new Config();
//单节点
config.useSingleServer().setAddress("redis://" + host + ":" + port);
if (StringUtils.isEmpty(password)) {
config.useSingleServer().setPassword(null);
} else {
config.useSingleServer().setPassword(password);
}

//添加主从配置
// config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""});

// 集群模式配置 setScanInterval()扫描间隔时间,单位是毫秒, //可以用"rediss://"来启用SSL连接
// config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001").addNodeAddress("redis://127.0.0.1:7002");
return Redisson.create(config);
}
}

一种非阻塞的使用方式形如

1
2
3
4
5
6
lock.tryLock();
try {
// ...
} finally {
lock.unlock();
}

这里没有显示设置锁的失效时间,默认持有锁30s,且由watch dog(看门狗)每隔10s续期一波,这里的自动续期,请重点关注,后面会说明因为它导致的锁无法释放

手动设置失效时间: tryLock(time, TimeUnit)

  • 当指定失效时间时,将没有看门狗的自动续期逻辑

一个具体的分布式锁使用姿势如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void lockReIn(int id) {
RLock rLock = redissonClient.getLock("lock_prefix_" + id);
if (rLock.tryLock()) {
try {
System.out.println("------- 执行业务逻辑 --------" + Thread.currentThread());
Thread.sleep(100);
System.out.println("------- 执行完毕 ----------" + Thread.currentThread());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
} else {
System.out.println("get lock failed for " + Thread.currentThread());
}
}

如果希望阻塞方式获取分布式锁时,使用RLock#lock()来替换RLock#tryLock()

2. 锁无法释放场景

重点关注下上面的自动续期方式,当我们使用姿势不对的时候,可能导致锁无法释放,那么什么样的场景会导致这个问题呢?

  • 主动释放锁异常失败
  • watch dog 一直存活,不断的续期

我们借助线程池来演示这个场景

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
// 固定线程池,且线程不会被回收,导致时而能重入获取锁,时而不行
ExecutorService executorService = Executors.newFixedThreadPool(2, new NamedThreadFactory("fixed-"));

// 普通线程池,空闲线程会被回收,这样就会导致将不会有其他业务方能获取到锁
ExecutorService customExecutorService = new ThreadPoolExecutor(0, 1,
1L, TimeUnit.MICROSECONDS,
new LinkedBlockingQueue<Runnable>(), new NamedThreadFactory("custom-"));

private void unLockFailed(int id) {
RLock rLock = redissonClient.getLock("lock_prefix_" + id);
if (rLock.tryLock()) {
try {
System.out.println("------- 执行业务逻辑 " + id + " --------" + Thread.currentThread());
} finally {
// 模拟释放锁失败
System.out.println(1 / 0);
rLock.unlock();
}
} else {
System.out.println("get lock failed for " + Thread.currentThread());
}
}

public void testLock() {
executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("threadId fix : " + Thread.currentThread().getId());
unLockFailed(2);
} catch (Exception e) {
e.printStackTrace();
}
}
});

customExecutorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("threadId custom : " + Thread.currentThread().getId());
unLockFailed(3);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}

在使用锁的业务逻辑中,释放锁时模拟了释放失败的case

两个线程池

  • 一个固定大小的线程池(线程不会被回收):再次访问时,之前持有锁的线程依然可以获取锁;另外一个不行
  • 一个普通的线程池(线程会被回收):将没有线程能持有锁

上面这种case最主要的问题在于redissonClient作为单实例,这个实例不回收,看门狗的续期任务也不会取消;因此即便持有锁的业务逻辑走完了,抛异常了,但是续期任务没有感知,依然在默默的执行,从而导致分布式锁一直无法释放,直到redissonClient实例销毁

小结

  • RedissonClient公用时,主动释放锁失败,但是注意看门狗的任务不注销,分布式锁一直续期,从而导致分布式锁无法有效释放

II. 其他

0. 项目

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到