参考Redisson-分布式锁

问题分析

在集群模式下或者分布式的系统下,存在多个JVM,不同JVM之间用的是不同的锁监视器,也就意味着可能产生并行而锁不生效的问题,也就导致了线程并发安全问题,为此就需要一种锁监视器,可以让不同的JVM之间使用,也就需要分布式锁

基本原理

  • 什么是分布式锁:满足分布式系统或者集群模式下多进程可见并互斥的锁
  • 特点:
    • 多进程可见
    • 互斥
    • 高可用
    • 高性能(高并发)
    • 安全性

分布式锁的实现(常见)

分布式锁实现比较

redis常见的实现之一就是通过Redisson

Redisson快速开始

  1. 引入依赖:
1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.2</version>
</dependency>
  1. 配置Redission客户端:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class RedissonConfig{
@Bean
public RedissonClient redissonClient(){
//配置类
Config config = new Config();
//添加redis地址,这里添加了单点的地址,也可以用config.userClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
//创建客户端
return Redisson.create(config);
}
}
  1. 使用Redisson的分布式锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Resouuce
private RedissonClient redissonClient

@Test
void testRedisson() throws InterruptedException{
//获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
//判断释放获取成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}

Redisson可重入锁原理

  • 可重入就是利用hash结构记录线程id和重入次数
  • 流程图如下:
    可重入锁原理

获取锁的Lua脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 锁不存在
if (redis.call('exists', key) == 0) then
-- 获取锁并添加线程标识,state设为1
redis.call('hset', key, threadId, '1');
-- 设置锁有效期
redis.call('expire', key, releaseTime);
return 1; -- 返回结果
end;
-- 锁存在,判断threadId是否为自己
if (redis.call('hexists', key, threadId) == 1) then
-- 锁存在,重入次数 +1,这里用的是hash结构的incrby增长
redis.call('hincrby', key, thread, 1);
-- 设置锁的有效期
redis.call('expire', key, releaseTime);
return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的Lua脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local key = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];
-- 如果锁不是自己的
if (redis.call('HEXISTS', key, threadId) == 0) then
return nil; -- 直接返回
end;
-- 锁是自己的,锁计数-1,还是用hincrby,不过自增长的值为-1
local count = redis.call('hincrby', key, threadId, -1);
-- 判断重入次数为多少
if (count > 0) then
-- 大于0,重置有效期
redis.call('expire', key, releaseTime);
return nil;
else
-- 否则直接释放锁
redis.call('del', key);
return nil;
end;

Redisson的锁重试和WatchDog机制

流程图:
锁重试、WatchDog机制

重试机制

  • 默认的锁超时时间是三十秒(WathchdogTimeout)
  • 利用消息订阅(PubSub)信号量的机制,实现等待、唤醒、获取锁失败的重试机制,每次都等待释放后再尝试,减少了cpu的无用消耗

WatchDog机制

问题产生:如果锁超时释放了,如何防止另一个在重试锁而不会误拿到锁
在获取锁成功后,调用了scheduleExpirationRenewal这个方法,当新的线程来的时候,会执行一个续约方法,其中有一个超时任务(Timeout task),就会在**十秒(releaseTime/3)**后执行,更新有效期,然后又递归,保证锁永不过期,一直到锁释放(unlock)的时候,就会去取消这个超时任务,删除干净

注意:如果自己设置了leaseTime则就不会开启看门狗

mutiLock原理-主从一致性

产生原因-原理

主从集群时,主节点宕机产生的锁不同步问题,就需要多个节点集群(可附带主从),这时候必须所有主节点都有锁才会返回

创建mutiLock

  1. 首页在redis中部署多个实例
  2. 在java中配置好后,注入redissonClient
  3. 使用lock = redissonClient.getMultilock(lock1,lock2,lock3);连锁

    lock1,lock2,lock3是注入实例的名称

缺点

  • 运维成本高
  • 实现复杂