Redis分布式锁使用,含Redisson源码分析
- 本文不收取任何什么费用,如果有显示什么VIP之类的,估计就是平台看我太久没上线,强行收费了
Redis分布式锁
- 在程序员自己简单的使用
set key value nx 超时时间
来实现锁,这个key 一般是所需要锁住的业务,如order:某个商品序号
,它的value会设计为一个UUID + 线程号
的方式
分布式锁Key的设置
- 为什么会这样设置key,因为key是为了区别不同业务下不同的锁,如果一个用户抢手机商品,一个用户抢优惠卷,他们两个肯定不是一把锁,所以需要将它们区分,区分的方式就采用key来区别,手机的Key
lock:phone:商品ID
,优惠卷的Keylock:coupon:优惠卷ID
分布式锁Value的设置
- 为什么这样设置value,试想一个场景,当一个线程正常获取了锁,但是出于某种原因,他被阻塞了(阻塞的原因可能是GC垃圾回收之类的原因),在阻塞的这段时间,它自己获得的锁超时了
- 在超时过后,另外的线程
set nx
的时候发现没有锁了,这个时候让新来的线程获取到了锁 - 但是这个时候,原来阻塞的线程从阻塞状态中恢复到了运行状态,在它正常执行完业务逻辑代码后,准备要释放锁,但是这个时候,它自己的锁其实是超时删除了的,Redis中并没有它对应的锁,有的是一个新来的线程的锁,那么它在释放锁的时候是直接将别人的锁释放掉了,这样,别的线程都能够进入对应的业务逻辑,就可能产生线程安全问题
出现以上场景的原因就是:在释放锁的时候,并没有检查这把锁是不是自己的就直接释放了,所以可能释放到了别的线程的锁,所以要在value处设置是哪个线程的锁,添加上UUID是因为保证在分布式的场景下的安全
实现
public class SimpleRedisLock{
// 锁业务key
private String name;
private StringRedisTemplate redisTemplate;
// 锁业务key前缀
private static final String KEY_PREFIX = "lock:";
// 锁业务value前缀
private static final String ID_PREFIX = UUID.randomUUID() + "-";
public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
this.name = name;
this.redisTemplate = redisTemplate;
}
/**
* 锁业务
* @param leaseTime 超时时间
* @param unit 时间单位
* @return 是否获取锁成功,成功返回true,失败返回false
*/
public boolean tryLock(int leaseTime, TimeUnit unit){
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, leaseTime, unit);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁
*/
public void unlock(){
String threadId = ID_PREFIX + Thread.currentThread().getId();
String id = redisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)){
redisTemplate.delete(KEY_PREFIX + name);
}
}
}
存在的问题
- 以上自己写了一个分布式锁,在绝大多数环境下是够用了,但仍然存在问题
- 如果一个线程,它是执行到了
unlock()
的时候并且判断完了if后,线程才被阻塞,也就是说,这个线程要是恢复了过来,它下一步就不再判断是不是自己的锁了,它直接就会对锁进行删除,这种场景出现的概率不大,但仍然是有可能的 - 为什么会出现这种原因呢,因为释放锁的过程应该要是原子性的,也就是中间不能停下来执行别的程序,而通过
Lua脚本
能够完成这种功能
编写lua脚本
-- 不过多介绍语法,这里讲的是Redis
if(redis.call('get', 'key') == ARGV[1]) then
return redis.call('del', KEYS[1])
end
return 0
改良后的实现
- 讲脚本拷贝到一个
resource
资源文件中创建一个lua文件
对unlock()
函数进行修改
public class SimpleRedisLock{
// 锁业务key
private String name;
private StringRedisTemplate redisTemplate;
// 锁业务key前缀
private static final String KEY_PREFIX = "lock:";
// 锁业务value前缀
private static final String ID_PREFIX = UUID.randomUUID() + "-";
private static DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
this.name = name;
this.redisTemplate = redisTemplate;
}
/**
* 锁业务
* @param leaseTime 超时时间
* @param unit 时间单位
* @return 是否获取锁成功,成功返回true,失败返回false
*/
public boolean tryLock(int leaseTime, TimeUnit unit){
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, leaseTime, unit);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁
*/
public void unlock(){
redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name), ID_PREFIX + Thread.currentThread().getId());
}
}
可以改进的点
- 上面的代码足够解决很多常见的问题了,但是还可以让这个锁更加的完善,可以有以下两点完善
- 这个锁无法重入
- 可重入锁(Reentrant Lock)是一种在同一个线程中可以多次获取的锁。它允许线程在已经持有锁的情况下再次获取该锁,而不会被自己锁住
- 锁有可能因为阻塞导致锁释放,而不是因为业务执行完毕而释放
- 这个锁无法重入
- 根据这两个点,可以对上面的锁再次进行完善,但是在实际工作过程中,不推荐自己编写这样的锁,现在已经有了现成的,大牛们帮我们写的框架
Redisson
Redisson
快速入门
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
- 配置Redisson客户端,尽管Redisson确实有SpringBoot的
starter场景启动器
,但是如果直接导入starter
的话,会导致Spring官方提供的redis配置
失效(被覆盖),为了保留Spring的配置,单独引入Redisson,并且单独写配置文件
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient {
// 配置类
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setPassword("123456");
return Redisson.create(config);
}
}
- 使用Redisson的分布式锁
- tryLock()函数有三种重载方式分别是
- 空参:不进行重试,并且锁30秒后会释放
- 双参:表示重试时间和重试时间的单位(秒、毫秒)
- 三参:表示重试时间和锁过期时间,时间单位
- tryLock()函数有三种重载方式分别是
@Resource
private RedissonClient redissonClient;
public testRedisson() throws InterruptException {
// 获取锁,指定锁的名字
RLock lock = redissonClient.getLock("anyLock");
// 尝试获取锁,参数为:获取锁的最大等待时间(期间会重试)、锁自动释放时间、时间单位
// 如果设置无参数,表示leaseTime = -1,锁自动释放时间是30秒
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
if(isLock) {
try{
System.out.println("执行业务");
}finally{
lock.unlock();
}
}
}
Redisson可重入锁原理
- 在上面编写的Redis分布式锁是无法完成可重入的操作,因为上锁的逻辑是当reids中没有值的话,才在redis中设置一个key,如果有了这个值就等待,所以无法再次获得锁,也就没有办法重入
- Redis重入的原理:
- 在Redis中不使用String类型当作锁,使用的Hash类型当作锁,Hash的key是需要
锁的业务名称
,而域的键是UUID + 线程值
,域的值是重入的次数
- 锁的
业务名字
和域的键
不过多介绍,和之前实现的逻辑是一样的,重点介绍这个多出来的域的值
- 业务逻辑转变为
- 当一个线程获取锁,先判断是否有这个key,如果没有就新建一个Hash类型
- 如果有这个key,那么判断它的field是不是为当前线程,如果是,value+1表示重入,允许进行加锁,如果需要释放锁,那么也需要判断field是不是本线程,如果是value-1,如果value=0表示能够删除key,如果不为0,则继续
- 在Redis中不使用String类型当作锁,使用的Hash类型当作锁,Hash的key是需要
Redisson锁重试机制原理
- 跟踪
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
中的tryLock(),会发现tryLock()只是接口所定义的一个接口,查找它的一个实现类RedissonLock
,查看它的tryLock()函数,以下列出它的部分源码
public class RedissonLock{
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// 将重试时间转化为毫秒,并且赋值给time(记住这个time的值是重试时间)
long time = unit.toMillis(waitTime);
// 记录锁第一次进来的时间
long current = System.currentTimeMillis();
// 得到想获得锁的线程ID
long threadId = Thread.currentThread().getId();
// 这一步是重要的一步,这个函数根据它的名字能够看出只是尝试去获得,返回的应该是一个时间
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
}
}
- 根据刚进来的源码,执行到了tryAcquire()这个函数去获取锁,接下来分析这个函数做了什么
public class RedissonLock{
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
// 根据这个函数名称发现它只是去尝试异步获取锁
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
}
- 注意,这个函数还是在
RedissonLock
这个类里面,并没有出这个类,但是还是不知道它是怎么去锁的,所以接下来跟tryAcquireAsync()
这个函数
public class RedissonLock{
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
// 我们在调用leaseTime的时候明显不为-1,所以进入if判断中
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// 执行到这个表示lease肯定为-1了,不然的话,已经在上面return了,后面再来分析后面这一段
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
}
- 这个函数仍然是在
RedissonLock
这个类里面,这里发现源码对leaseTime进行判断了,可能会觉得奇怪,为什么leaseTime会要进行判断呢?- 如果对上面的快速入门还有印象的话,就知道,在我们没有设置的时候默认leaseTime = -1,可以自行跟以下源码,最后都会调用到上面的函数
- 因为我们这里自己设置了leaseTime,所以肯定是会走到第一个if后,
直接返回
,直接返回这一点很重要,因为如果我们没有设置的话,Redisson其实是会帮我们做一点事情的,如果直接在if返回,后续Redisson就无法帮我们做事,因为它已经return了 - 我们先看if里面到底是做了什么后,再看Redisson帮我们做了什么,接下来跟tryLockInnerAsync()函数
public class RedissonLock{
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
// 找了一个变量,存储了leaseTime的时间
internalLockLeaseTime = unit.toMillis(leaseTime);
// 执行lua脚本
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 需要记住的是,脚本里面的KEYS[]和ARGV[]的值,其实是跟在它后面的,和前面的无关
// KEYS的参数在Collections.singletonList()里面
// ARGV的参数是Collections.singletonList()后面的不定长参数
// 通过redis判断的getName()其实是在我们
// RLock lock = redissonClient.getLock("anyLock");这里的"anyLock"
// 也就是说先在redis中判断这个key是否存在,如果为0表示不存在,1表示存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
// key不存在了,表示没人来获取过,所以肯定获取锁
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 再为key设置一个毫秒过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
// 成功获取锁,返回null
"return nil; " +
// if结束
"end; " +
// 下一个if,到这里表示这个key肯定存在了,也就是有线程锁住了,这个时候判断这个锁是不是自己锁的
// 判断获取锁的线程是不是这个要来获取的线程,也就是看看能不能重入
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 进来了if,表示想要重入,所以value+1
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 充值key的过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
// 成功获取锁,返回null
"return nil; " +
// if结束
"end; " +
// 到这里表示这个key存在,并且锁资源的不是它本身,返回的是别人还需要锁住这个key多久
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
}
- 这个函数还是在RedissonLock类里面,这里需要注意的点其实是,它找了一个实例变量,存储了这个锁的过期时间,
需要记住有这么一回事
,存储后执行了lua脚本,为了保证获取锁的原子性,这里还需要记得,如果返回null
表示获取锁成功,如果返回的是毫秒数,表示别人占用这个锁的时间
,接下来返回到上面的函数
public class RedissonLock{
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
// 刚刚将这个if里面的东西判断完了,走的if里面
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// 现在想看看如果没走if是什么样
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
}
- 记住,上面执行完if里面的东西后,直接就返回了,没有下面什么事情,这里分析下面这一段是因为想看看如果是没有设置过期时间,Redisson会为我们做些什么而已
- 可以看到它同样是调用了tryLockInnerAsync()这个函数,他和上面不同的点在于
leaseTime
的设置,Redisson为我们赋值的是:commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()
,剩下的其实都是一摸一样的,也就是返回值,如果是null
表示成功获取到了锁,如果是毫秒数,表示获取锁失败,并且这个时间是别人还需要锁这个资源多久,记住,是别人还需要锁多久
,那么Redisson赋值的是什么?- 这里不列出来了,直接告诉各位,就是30秒,也就是说,直到这一步,默认的-1,才设置成30秒,这个Redisson为我们设置的,是一个叫做
看门狗
的东西,设置完以后,后面这一段暂且不看,这一段是Redisson为我们设置的看门狗的功能
- 这里不列出来了,直接告诉各位,就是30秒,也就是说,直到这一步,默认的-1,才设置成30秒,这个Redisson为我们设置的,是一个叫做
- 从这里返回的是这里的代码,下面看看后面又做了些什么
public class RedissonLock{
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// 将重试时间转化为毫秒,并且赋值给time
long time = unit.toMillis(waitTime);
// 记录锁第一次进来的时间
long current = System.currentTimeMillis();
// 得到想获得锁的线程ID
long threadId = Thread.currentThread().getId();
// 这一步是重要的一步,这个函数根据它的名字能够看出只是尝试去获得,返回的应该是一个时间
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
}
}
public class RedissonLock{
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
// 刚刚从这里返回来
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 提到过,获取锁返回的null,如果是null,表示获取锁成功,返回true
if (ttl == null) {
return true;
}
// 这里表示没有获取锁成功
// 计算上面获取这把锁所消耗的时间,并且用重试的时间 - 获取这把锁所消耗的时间
// 减去后是剩下的重试时间
time -= System.currentTimeMillis() - current;
// 重试的时间 - 获取这把锁所消耗的时间 如果 < 0表示已经没有时间重试了,直接返回失败
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 如果还有时间,再记录现在的时间
current = System.currentTimeMillis();
// 这里看到了一个subscribe,表示订阅,这个订阅是在锁释放后会发布的一个订阅
// 后续会查看到代码
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 在time时间,也就是重试的时间内,如果发现有线程发布了主题,才会往下继续执行
// 如果没有发布对应的主题,表示这个线程无法再进行重试,进入if语句
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
// 取消订阅发布的主题
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
// 返回无法获得锁
return false;
}
// 到这里表示收到对应的锁释放的主题
try {
// 这里重新记录剩下的重试时间
time -= System.currentTimeMillis() - current;
// 如果重试时间已经 < 0表示它没办法再去获取锁了
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 这里是真正的去获得锁
while (true) {
long currentTime = System.currentTimeMillis();
// 因为前面是收到了锁同一个资源的锁释放的消息
// 这里才会重新尝试去获取锁
// 到这里才算是第二次尝试获取
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 如果是null表示获取到了
// 不为null表示这个ttl是锁同一资源的key的剩余时间
if (ttl == null) {
return true;
}
// 重新计算重试的剩余时间
time -= System.currentTimeMillis() - currentTime;
// 如果重试时间<0表示无法获得锁
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 保存当前时间
currentTime = System.currentTimeMillis();
// 记住ttl是锁同一资源的key的剩余时间
// 同一把锁的时间 > 0,并且同一把锁的时间 < 我剩下的重试时间走if里面
if (ttl >= 0 && ttl < time) {
// 等待一个ttl的时间,也就是等锁一释放,执行定时任务,也就是抢夺锁
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
// 到这里只有一个可能,那就是锁释放的时间已经超过了重试的时间
// 那么我在别人还没有释放锁的时候,并且在我重试时间到了以后,最后尝试获取一次
// 获得的到的话就执行任务,获取不到就不执行
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
// 最后计算这次获取锁所耗费的时间
// 并且计算出剩余的重试时间
time -= System.currentTimeMillis() - currentTime;
// 重试时间 < 0表示已经无法获取锁了,退出函数
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
// 出现任何情况,都会取消掉订阅的消息
unsubscribe(subscribeFuture, threadId);
}
}
}
- 这就是Redisson的锁重试机制的原理,里面使用了发布订阅的方式,并且通过定时任务,让其不会一直无条件的死循环,降低了CPU的压力
Redisson看门狗机制原理
- 根据上面的源码分析后,不知道是否还记得,上面还有一个点没有讲诉到,就是之前提到的Redisson在我们没有设置leaseTime的时候,leaseTime被赋值-1,根据-1判断后设置了30秒给leaseTime,后面的代码并没有进行分析,就直接跳过了,现在再来对其进行分析
public class RedissonLock{
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
// 我们在调用leaseTime的时候明显不为-1,所以进入if判断中
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// 执行到这个表示lease肯定为-1了,不然的话,已经在上面return了,后面再来分析后面这一段
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
// 尝试获取完锁后,不管有没有获取到锁,都会走下面的逻辑
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
if (ttlRemaining) {
// 这里执行了一个超时续约
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
}
- 在Redisson为我们设置的超时时间可以看到,不管是否能够获取到锁,它都会执行一个超时续约的函数,接下来主要分析这个函数具体干了什么
public class RedissonLock{
private void scheduleExpirationRenewal(long threadId) {
// 这里new了一个entry,entry就是和hashmap里面那个entry是一样的
// 存储的是Map里面键值对的值
ExpirationEntry entry = new ExpirationEntry();
// 这里的EXPIRATION_RENEWAL_MAP其实是juc里面的ConcurrentHashMap
// 具体功能不再说明,感兴趣可以自行查找资料
// put进去的值是一个函数getEntryName()
// 这个函数会返回一个entryName,这个实例变量的值是线程的id:new Redisson所传入的name
// 这个的返回值,如果这个entryName不存在的话,也就是第一次进入这个函数,返回null
// 如果存在,返回的就是之前的oldEntry,也就不止一次进入这个函数,也就不为null
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
// 进来这里表示之前之前来过
// 回忆一下,进来这里表示之前已经来过一次了
// 那这里是怎么进来的?
// 能到这里表示之前就尝试锁了,这里是第二次想尝试获取了
// 然后为entry中放入一个线程的ID
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
// 如果是第一次来,那么不仅需要放一个线程ID
// 还执行了一个超时续约,接下来对这个函数进行分析
renewExpiration();
}
}
}
public class RedissonLock{
private void renewExpiration() {
// 这里通过ConcurrentHashMap获取线程对应的entry
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 这里开启一个异步线程,执行任务,先不看里面具体内容
// 查看这个线程的后两个参数internalLockLeaseTime / 3, TimeUnit.MILLISECONDS
// 表示这个异步任务,每经过三分之一的internalLockLeaseTime就会执行一次
// 那么,是否记得这个internalLockLeaseTime是什么呢
// 这个值就是在上锁的时候提到的
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// reschedule itself
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
public class RedissonLock{
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
// 就是这里,当初讲解的时候让记住的,如果忘记了,请再认真看一遍完整的流程
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
}
public class RedissonLock{
private void renewExpiration() {
// 这里通过ConcurrentHashMap获取线程对应的entry
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 这里开启一个异步线程,执行任务,先不看里面具体内容
// 查看这个线程的后两个参数internalLockLeaseTime / 3, TimeUnit.MILLISECONDS
// 表示这个异步任务,每经过三分之一的internalLockLeaseTime就会执行一次
// 那么,是否记得这个internalLockLeaseTime是什么呢
// 这个值就是在上锁的时候提到的
//
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 异步线程主要里面有一个这个方法,点进去查看一下
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// reschedule itself
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
public class RedissonLock{
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
}
- 直接说明这个Lua脚本的意思,表明对一个Redis中的Key,重置它的过期时间为internalLockLeaseTime
public class RedissonLock{
private void renewExpiration() {
// 这里通过ConcurrentHashMap获取线程对应的entry
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 这里开启一个异步线程,执行任务,先不看里面具体内容
// 查看这个线程的后两个参数internalLockLeaseTime / 3, TimeUnit.MILLISECONDS
// 表示这个异步任务,每经过三分之一的internalLockLeaseTime就会执行一次
// 那么,是否记得这个internalLockLeaseTime是什么呢
// 这个值就是在上锁的时候提到的
//
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 异步线程主要里面有一个这个方法,点进去查看一下
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// 递归执行自己本身
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
- 看到了递归执行,但是其实不用慌,这个递归有前提的,就是根据Redisson为我们所设置的internalLockLeaseTime,也就是30秒,除以3,也就是10秒,这个递归才会开始执行一次,每一次的执行,都是会刷新这个锁的过期时间,让他回到30秒,这样做的好处是,一个线程如果释放了锁,不会是因为线程阻塞的原因导致的,只有可能是业务执行完毕
到这里,其实Redisson的看门狗机制就讲解完毕了,总结来就是,当第一次获取锁时,会开启一个异步任务,这个任务会不断的刷新锁的时候,防止因为线程阻塞的原因,导致锁释放,如果是第二次进入,则不会再开启这个刷新任务了