可以看到,在这里的时候,获取到的 thredId 就是 1。那 key 里面 UUID 后面拼接的 1。是不是就是这里来的呢?我们接着往下看。
再往前 Debug 三步就能到下面的这个位置:
org.redisson.RedissonLock#tryLockInnerAsync
到这里的 getLockName(threadId) 其实就是我们要找的东西:
你看,这一串东西,不就是我们刚刚看到的 UUID:1 吗?这个 1 就是线程ID。
什么?你问我为什么说这个 id 是 UUID?
直觉,程序猿的直觉告诉我,这就是个 UUID。但是我可以给你验证一下。
这个 id 的来源是下面这个接口:
org.redisson.connection.ConnectionManager
而该接口有 5 个实现类:
在创建 ConnectionManager 时,每个实现类的构造方法传的都是 UUID。
所以,我们可以下结论了:
使用 Redssion 做分布式锁,不需要明确指定 value ,框架会帮我们生成一个由 UUID 和 加锁操作的线程的 threadId 用冒号拼接起来的字符串。
毫无挑战甚至有点无聊的探索过程啊。(其实我想表达的是源码真的不难,不要抱有恐惧的心理,带着问题去看源码。)
但是别着急,这只是开胃菜。
对于第二个问题:为什么要用 Hash 类型,而不用 String 类型呢?
我们在下一节,寻找过期时间去哪里了的同时,寻找该问题的答案。
过期时间去哪了?
这个问题,我们从这段代码里面可以找到答案:
org.redisson.RedissonLock#tryLockInnerAsync
我们首先看一下这个方法对应的几个入参:
主要关注我框起来的部分:
script:是要执行的 lua 脚本。
keys:是 redis 中的 key。这里的 why 就是 KEYS[1]。
params:是 lua 脚本的参数。这里的 30000 就是 ARVG[1]。UUID:thredId 就是 ARVG[2]。
所以这个过期时间我们也知道了,默认是 30000ms,即30s。
知道了上面三个参数的含义后,我们再来拆解这个 lua 脚本就很简单了,首先我们把他拆解为三部分:
第一部分:加锁
先看第一部分的加锁操作:
第 4行,首先用 exists 判断了 KEYS[1] (即 why)是否存在。
如果不存在,则进入第 5 行,使用 hincrby 命令。hincrby 命令是干什么的知道吧?
之后进入第 6 行,对 KEY[1] 设置过期时间,30000ms。
然后,第7行,进行返回为 nil,结束。
这样,一个原子性的加锁操作就完成了。
到这里,我们就已经从源码的角度验证了:因为用的是 hincrby 命令,Redssion 做锁的时候 key 确实是一个 Hash 结构。
第二部分:重入
当第一部分的 if 分支判断 KEYS[1] 是存在的,则会进入到这个分支中:
由于 KEYS[1] 是一个 Hash 结构,所以第 13 行的意思是获取这个 KEYS[1] 中字段为 ARGV[2] 的数据,判断是否存在。
如果存在,则进入第 14 行代码,用 hincrby 命令对 ARGV[2] 字段进行加一操作。
然后第 15 行,没啥说的,就是重新设置过期时间为 30s。之后第 16 行,返回为 nil,结束
所以,你在感受一下第 14 行代码的作用是什么?进入,然后加一,你联想到了什么?
看到这里的时候,解锁的 lua 脚本都不必看的,想也能想到,肯定是有一个减一的操作,然后减到 0,就释放这把锁。一会我们就去验证这个点。
所以,这里也就解释了为什么 Redssion 需要用 Hash 类型做锁。因为它支持可重入呀。
你用 String 类型,你怎么实现重入功能,来键盘给你,实现一个,让我学习一下?(其实也是可以的,就是有点背道而驰了。没意义。)
第三部分:返回
一行代码,问题不大。作用就是返回 KEY[1] 的剩余存活时间
通过分析 lua 的这三部分,我们知道了:过期时间默认是 30s。当一个 key 加锁成功或者当一个锁重入成功后都会返回空,只有加锁失败的情况下会返回当前锁剩余的时间。
记住这个结论,我们在接下来的看门狗咋工作的这一小节中会用到这个返回值。
另外,写文章的时候我发现 Redssion 的最新版本 3.12.3 和之前的版本相比,加锁时的 lua 脚本有一个细微的差别,如下:
3.12.3 版本之前用的是 hset ,现在用的是 hincrby。所以导致第一部分和第二部分相似度有点高。看起来会有点容易迷糊。
你去网上找应该看到的都是说 hset 操作的。因为 3.12.3 版本刚刚发布一个月。
恭喜你,朋友,又学到了一个用不上的知识点。
看门狗咋工作的?
看到这一节的朋友们,辛苦了。在这一节,我们终于要看到看门狗长啥样了。
org.redisson.RedissonLock#tryAcquireAsync
这里的 ttlRemaining 就是经过 lua 脚本后返回的值。经过前面我们知道了,当加锁成功或者重入成功后会返回 null。进入这个方法:
org.redisson.RedissonLock#scheduleExpirationRenewal
这个方法,就是看门狗工作的片区了。
Debug之后,你会遇到这个方法:
org.redisson.RedissonLock#renewExpiration
很明显,从上面标注的数字可以看出来:
①:这是一个任务。
②:这任务需要执行的核心代码。
③:该任务每 internalLockLeaseTime/3ms 后执行一次。而 internalLockLeaseTime 默认为 30000。所以该任务每 10s 执行一次。
接着我们看一下 ② 里面执行的核心代码是什么:
这个 lua 脚本,先判断 UUID:threadId 是否存在,如果存在则把 key 的过期时间重新设置为 30s,这就是一次续命操作。
来,在做个小学二年的算法题:
应用题:key 默认的过期时间是 30s,每过 30s/3 的时候会去进行续命操作,那么每当 key 的 ttl(剩余时间)返回多少的时候,会进行续命操作?
答:由题干可知,30s/3 = 10s。于是得公式到:30s - 10s =20s。
所以,每当 key 的 ttl(剩余时间)为 20 的时候,则进行续命操作,重新将 key 的过期时间设置为默认时间 30s。
注意我上面一直强调的是默认时间 30s。
因为这个时间是可以修改的,比如我们想要修改为 60s,就这样:
于是 internalLockLeaseTime 就变成了 60000 了: