16.redisson分布式锁使用及注意事项
约 1140 字大约 4 分钟
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. 其他
0. 项目
Loading...