某次偶然听到redission看门狗,感觉比较有趣,于是就想看看它长啥样。。。。废话不多说,直入正题。

什么是看门狗?
用官方文档的话来说就是:

大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

首先先看看这部分实例的代码,相信大家都能看懂

Redisson 开启看门狗 redisson看门狗原理_看门狗


看了这部分代码之后,探究之前再思考一个问题:

问题:redission实现的分布式锁和我们自己造的轮子有什么区别?

如果是自己造轮子基于redis实现加锁和解锁的话,它的实现如下:

  • 加锁:原子命令加锁(实际就是向redis用setnx原子命令设置一个随机值)
  • 解锁:释放锁的时候检查这个值是否存在,存在就删除,但是这个步骤包含两个操作,需要保证这两步操作的原子性,1.通过key获取这个随机值,判断这个值是否存在 2.删除这个值。

如果不保证原子性会发生什么问题呢?这里假设一种情况:

  1. 线程A准备释放锁,首先要获取这个锁,获取到锁,正准备删除
  2. 但此时因为一些原因导致锁超时,如程序GC导致了STW,没来得及删除,锁过期了
  3. STW之后,线程B进来加了同样key的锁,此时线程B还没执行完,线程A又执行了刚刚没有执行完的命令,把线程B的锁删除了,这就出问题了。

那redission又是如何实现加锁和解锁的呢?

(1)如何加锁:对于这个问题,首现确定向redis中set进一个值这一步肯定是框架帮我们生成了,所以我们要想验证这种情况,除了从源码中查看,也可以直接程序跑起来,到reids管理界面看一下

Redisson 开启看门狗 redisson看门狗原理_Redisson 开启看门狗_02


可以看到这是一个hash类型的值,key是一个随机值(UUID:线程id),value随便弄了一个1进去。而通过debug 源码发现通过tryLockInnerAsync方法发送了一段lua脚本,当加锁成功后会返回null,注意这个返回null后文会用到.

Redisson 开启看门狗 redisson看门狗原理_Redisson 开启看门狗_03


而这个getLockName方法就是我们看到的key值

Redisson 开启看门狗 redisson看门狗原理_加锁_04


我们顺带点进去看看evalWriteAsync这个方法

Redisson 开启看门狗 redisson看门狗原理_看门狗_05

script:是要执行的 lua 脚本。
keys:是 redis 中的 key。这里的 why 就是脚本中的 KEYS[1]。
params:是 lua 脚本的参数。这里的 30000 就是脚本中的 ARVG[1]。UUID:thredId 就是 ARVG[2]。

所以这个过期时间我们也知道了,默认是 30000ms,即30s。

让我们回过头看看这段脚本的含义:

Redisson 开启看门狗 redisson看门狗原理_看门狗_06

第一部分:加锁

  • 首先用 exists 判断了 KEYS[1] (即 why)是否存在。
  • 如果不存在,则进入第 5 行,使用 hincrby 命令创建一个新的哈希表,如果域field不存在,那么在执行命令前会被初始化为0,此命令的返回值就是执行hincrby命令后,哈希表key中域field的值,此时进行increment,也就是返回1
  • 之后进入第6行,对KEY[1]设置过期时间,30000ms
  • 然后返回nil

第二部分:重入

  • 首先判断KEY[1]是否存在,因为KEY[1]是一个hash结构,所以13行意思是获取这个KEYS[1]中字段为ARGV[2]也就是UUID:thredId这个值是否存在
  • 如果存在进入14行代码对其进行加1操作(锁重入)
  • 然后进入15行重新设置过期时间30s
  • 然后返回nil

第三部分:返回

  • 作用就是返回 KEY[1] 的剩余存活时间

(2)如何解锁:使用了lua脚本+Redis单线程
为什么 lua 脚本可以解决这个问题呢?
因为 lua 脚本的执行是原子性的,它会将这获取这个值和删除值两个操作放到一个脚本中,当成一个命令去执行,再加上 Redis 执行命令是单线程的,所以在 lua 脚本执行完之前,其他的命令都得等着。就不会出现上面说的情况了。

刚看完了加锁操作的lua脚本,来看解锁操作的lua脚本也就很清晰明了了

Redisson 开启看门狗 redisson看门狗原理_Redisson 开启看门狗_07

  • 首先判断KEYS[1]是否存在
  • 存在将值减1,如果counter还大于0,就重新设置过期时间30000ms,否则就删除操作

可以看到删除过后还执行了一个publish命令,其实这里是基于redis的一个发布/订阅功能,解锁的时候发布了一个事件,通知其他线程,我这边锁用完了,你们可以用了,那其他线程是什么线程呢?也就是订阅了这把锁的线程

Redisson 开启看门狗 redisson看门狗原理_看门狗_08

这里可以看到当ttl不等于null的时候也就是加锁失败,加锁失败的线程,都会去执行subscribe方法,这里就和publish对应上了

以上就是redission加锁和解锁的一个实现原理,讲了怎么多那看门狗机制怎么实现的呢?

当我们调用lock方法后都要调用下面这个方法:

org.redisson.RedissonLock#tryAcquireAsync

Redisson 开启看门狗 redisson看门狗原理_加锁_09

scheduleExpirationRenewal方法从字面上意思就很容易理解到期续订,也就是看门狗的具体实现。那什么情况下走到else这个条件呢,也可以理解成什么情况下开启看门狗呢?
答:首先leaseTime要==-1,这个leaseTime也就是设置的锁过期时间,也就是说如果我们调用的lock方法传入超时时间限制,也就不会开启开门狗。

lock.lock(); 开启看门狗
lock.lock(5000, TimeUnit.SECONDS); 不开启看门狗

其次ttlRemaining==null,这个ttlRemaining也就是加锁成功后上文提到的返回的null值。

再次debug进入这个方法后,会进入到下面这个方法

org.redisson.RedissonLock#renewExpiration

Redisson 开启看门狗 redisson看门狗原理_加锁_10

很明显,从上面标注的数字可以看出来:
①:这是一个定时任务。
②:这任务需要执行的核心代码。
③:该任务每 internalLockLeaseTime/3ms 后执行一次。而 internalLockLeaseTime 默认为 30000。所以该任务每 10s 执行一次。

在看一下第二步干的什么

Redisson 开启看门狗 redisson看门狗原理_看门狗_11

所以,每当 key 的 ttl(剩余时间)为 20 的时候,则进行续命操作,重新将 key 的过期时间设置为默认时间 30s,当然这个internalLockLeaseTime值也是可以修改的,如果改成60秒,那么每当 key 的 ttl 返回 40 (60 -60/3)时,会进行续命操作

Redisson 开启看门狗 redisson看门狗原理_加锁_12

写在最后的话:

到这里看门狗的具体实现也就清楚了,无非是后台起一个定时任务的线程,每隔一定时间对该锁进行续命,延长锁的时间,很多人肯定好奇,那延长锁的次数是有限制的吗?难道无限进行续命吗,假设业务一直没执行完,难道锁一直不释放吗?起初我也有这样的疑问,但是想了想,实际业务中也不能发生这样的情况,除非是代码bug,或者陷入了死循环,所以这也不能怪到redission上面。

参考文章:
1.https://github.com/redisson/redisson/wiki/目录