Redis学习之Redisson实现分布式锁
Redisson实现分布式锁
Redisson 是 Java 的 Redis 高级客户端,提供了各种现成的分布式工具类便于我们使用 Redis。
官网:https://github.com/redisson/redisson
中文文档:https://github.com/redisson/redisson/wiki/
使用方式:
1)引入独立的Redisson包:
不建议引入 springboot-starter,因为可能会和 springboot 内置的 redis 整合冲突
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.1</version>
</dependency>
2)创建一个Redisson客户端,代码如下:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379")
.setPassword("123321");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
3)使用Redisson的Lock,代码如下:
@Resource
private RedissionClient redissonClient;
@Test
void testRedisson() throws Exception{
//获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断获取锁成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}
如何实现可重入锁
目的:保证同一个线程可以多次获取同一把锁
解决思路:在锁的 value 中额外保存当前线程获取锁的次数,每次获取锁 +1、释放锁 -1,当次数为 0 时才真正删除 key。
采用hash结构来存储锁信息,如图:
流程如下:
注意:
-
所有的判断和操作都需要使用Lua脚本来保证原子性
-
每次获取和释放锁时要重置锁的有效期。
获取锁的Lua脚本:
释放锁的Lua脚本:
如何重试获取锁
基于 Redis Pub / Sub 发布订阅机制。
如何防止锁提前超时释放
基于看门狗机制
总的来说就是默认锁过期时间是30s,而自动续期机制在源码当中就是开启了定时任务,定时间隔是看门狗时间的三分之一,也就是10s,所以就是在业务没有处理完的情况下锁默认每隔10s续期到30s;
需要思考两个问题:
-
如何保证同一个锁只注册一个定时任务?
-
如何防止无限续期?
要解决这些问题,使用全局 ConcurrentHashMap 来管理锁 => 任务信息,key 为锁的 id,从而保证唯一。当某个锁释放时,从全局 ConcurrentHashMap 中取出定时任务并取消掉,然后把锁的信息从 Map 中删掉即可。
最终,完整的分布式锁流程如下:
如何解决主从一致性问题
为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例
此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。
可以使用Redisson的MultiLock(联锁)来解决,和核心思想是开启多个Redis 主节点,设置锁时必须在所有主节点都写入成功,才算设置成功。如果出现有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。
MutiLock 加锁原理
当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.
实现 MultiLock 的几个关键:
-
遍历所有节点,依次设置锁,并使用列表来记录所有主节点的锁是否设置成功。
-
只要有一个节点设置不成功,就要释放所有的锁,从头来过。
-
因为不同节点设置锁成功的时间不同,所以在所有锁设置成功后,要统一设置过期时间(但如果 leaseTime = -1 就不用了,因为开启了看门狗机制会自动续期)
-
锁释放时间(leaseTime)必须要大于抢锁最大等待时间(waitTime),否则可能出现第一个节点抢到锁,最后一个节点还没抢到锁,之前的锁就已经超时释放了。所以如果指定了 waitTime 和 leaseTime,默认 leaseTime = waitTime * 2。
MultiLock最安全,但成本也是最高的。