某次偶然听到redission看门狗,感觉比较有趣,于是就想看看它长啥样。。。。废话不多说,直入正题。
什么是看门狗?
用官方文档的话来说就是:
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
首先先看看这部分实例的代码,相信大家都能看懂
看了这部分代码之后,探究之前再思考一个问题:
问题:redission实现的分布式锁和我们自己造的轮子有什么区别?
如果是自己造轮子基于redis实现加锁和解锁的话,它的实现如下:
- 加锁:原子命令加锁(实际就是向redis用setnx原子命令设置一个随机值)
- 解锁:释放锁的时候检查这个值是否存在,存在就删除,但是这个步骤包含两个操作,需要保证这两步操作的原子性,1.通过key获取这个随机值,判断这个值是否存在 2.删除这个值。
如果不保证原子性会发生什么问题呢?这里假设一种情况:
- 线程A准备释放锁,首先要获取这个锁,获取到锁,正准备删除
- 但此时因为一些原因导致锁超时,如程序GC导致了STW,没来得及删除,锁过期了
- STW之后,线程B进来加了同样key的锁,此时线程B还没执行完,线程A又执行了刚刚没有执行完的命令,把线程B的锁删除了,这就出问题了。
那redission又是如何实现加锁和解锁的呢?
(1)如何加锁:对于这个问题,首现确定向redis中set进一个值这一步肯定是框架帮我们生成了,所以我们要想验证这种情况,除了从源码中查看,也可以直接程序跑起来,到reids管理界面看一下
可以看到这是一个hash类型的值,key是一个随机值(UUID:线程id),value随便弄了一个1进去。
而通过debug 源码发现通过tryLockInnerAsync方法发送了一段lua脚本,当加锁成功后会返回null,注意这个返回null后文会用到.
而这个getLockName方法就是我们看到的key值
我们顺带点进去看看evalWriteAsync这个方法
script:是要执行的 lua 脚本。
keys:是 redis 中的 key。这里的 why 就是脚本中的 KEYS[1]。
params:是 lua 脚本的参数。这里的 30000 就是脚本中的 ARVG[1]。UUID:thredId 就是 ARVG[2]。
所以这个过期时间我们也知道了,默认是 30000ms,即30s。
让我们回过头看看这段脚本的含义:
第一部分:加锁
- 首先用 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脚本也就很清晰明了了
- 首先判断KEYS[1]是否存在
- 存在将值减1,如果counter还大于0,就重新设置过期时间30000ms,否则就删除操作
可以看到删除过后还执行了一个publish命令,其实这里是基于redis的一个发布/订阅功能,解锁的时候发布了一个事件,通知其他线程,我这边锁用完了,你们可以用了,那其他线程是什么线程呢?也就是订阅了这把锁的线程
这里可以看到当ttl不等于null的时候也就是加锁失败,加锁失败的线程,都会去执行subscribe方法,这里就和publish对应上了
以上就是redission加锁和解锁的一个实现原理,讲了怎么多那看门狗机制怎么实现的呢?
当我们调用lock方法后都要调用下面这个方法:
org.redisson.RedissonLock#tryAcquireAsync
scheduleExpirationRenewal方法从字面上意思就很容易理解到期续订,也就是看门狗的具体实现。那什么情况下走到else这个条件呢,也可以理解成什么情况下开启看门狗呢?
答:首先leaseTime要==-1,这个leaseTime也就是设置的锁过期时间,也就是说如果我们调用的lock方法传入超时时间限制,也就不会开启开门狗。
lock.lock(); 开启看门狗
lock.lock(5000, TimeUnit.SECONDS); 不开启看门狗
其次ttlRemaining==null,这个ttlRemaining也就是加锁成功后上文提到的返回的null值。
再次debug进入这个方法后,会进入到下面这个方法
org.redisson.RedissonLock#renewExpiration
很明显,从上面标注的数字可以看出来:
①:这是一个定时任务。
②:这任务需要执行的核心代码。
③:该任务每 internalLockLeaseTime/3ms 后执行一次。而 internalLockLeaseTime 默认为 30000。所以该任务每 10s 执行一次。
在看一下第二步干的什么
所以,每当 key 的 ttl(剩余时间)为 20 的时候,则进行续命操作,重新将 key 的过期时间设置为默认时间 30s,当然这个internalLockLeaseTime值也是可以修改的,如果改成60秒,那么每当 key 的 ttl 返回 40 (60 -60/3)时,会进行续命操作
写在最后的话:
到这里看门狗的具体实现也就清楚了,无非是后台起一个定时任务的线程,每隔一定时间对该锁进行续命,延长锁的时间,很多人肯定好奇,那延长锁的次数是有限制的吗?难道无限进行续命吗,假设业务一直没执行完,难道锁一直不释放吗?起初我也有这样的疑问,但是想了想,实际业务中也不能发生这样的情况,除非是代码bug,或者陷入了死循环,所以这也不能怪到redission上面。
参考文章:
1.https://github.com/redisson/redisson/wiki/目录
2.https://juejin.cn/post/6844904106461495303