3

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

 3 years ago
source link: https://spring.hhui.top/spring-blog/2021/03/01/210301-SpringBoot%E7%B3%BB%E5%88%97redisson%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E4%BD%BF%E7%94%A8%E5%8F%8A%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

I. 项目环境

1. pom依赖

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

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

<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实例

spring:
redis:
host: 127.0.0.1
port: 6379
password:

II. 分布式锁

1. 使用姿势

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

@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);
}
}

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

lock.tryLock();
try {
// ...
} finally {
lock.unlock();
}

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

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

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

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

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 一直存活,不断的续期

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

// 固定线程池,且线程不会被回收,导致时而能重入获取锁,时而不行
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. 其他

1. 一灰灰Blog

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

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

一灰灰blog

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK