Redisson 工作原理-源码分析

  • 作者: 凯哥Java
  • redis
  • 时间:2021-08-25 10:06
  • 163人已阅读
简介 1:Redisson是什么个人理解:一种可重入、持续阻塞、独占式的分布式锁协调框架,可从ReentrantLock去看它。①:可重入拿到锁的线程后续拿锁可跳过获取锁的步骤,只进行value+1的步骤。②:持续阻塞获取不到锁的线程,会在一定时间内等待锁。日常开发中,应该都用过redis的setnx进行分布式的操作吧,那setnx返回了false我们第一时间是不是就结束了?因此redisson优化了这

1:Redisson 是什么

个人理解:一种 可重入、持续阻塞、独占式的 分布式锁协调框架,可从 ReentrantLock 去看它。

①:可重入
拿到锁的线程后续拿锁可跳过获取锁的步骤,只进行value+1的步骤。

②:持续阻塞
获取不到锁的线程,会在一定时间内等待锁。

日常开发中,应该都用过redis 的setnx 进行分布式的操作吧,那setnx 返回了false我们第一时间是不是就结束了?
因此redisson 优化了这个步骤,拿不到锁会进行等待,直至timeout 。

③:独占式
很好理解,同一环境下理论上只能有一个线程可以获取到锁。

对于redis 集群模式下,若master 的锁还没有同步给slave,这时 master 挂掉,然后哨兵选举出新的master,
由于新的 master 并没有同步到锁,所以这个时候其他的线程仍然能获取到锁。因此独占式在一定条件下是会失效的。
这个观点在另外几篇参考的文章中也有提到,个人也比较赞同,因此在此写个笔记。

2:示例代码

redisson的GitHub地址:https://github.com/redisson/redisson
我用的是boot-starter,配置参考官网给出的就行了。

测试代码块:

e9950e1484b025c5bc655b6d34537704.png

是不是和ReentrantLock 很像呢?

贴上我画的草图再讲后面的内容:

497caebd46ebe39d54003c8bda89b7c7.png

3:如何获取锁

获取锁的操作采用lua脚本的形式,以保证指令的原子性。

299da1c242f8b96bce1642ecf8e2458b.png

从截图上的序号来说步骤:
①:如果锁不存在,则进行hincrby 操作(key不存在则value等于1,占锁),并设置过期时间,然后返回nil。
②:如果锁存在且 key 也存在,则进行hincrby操作(可重入锁思想),并以毫秒为单位重新设置过期时间(续命),然后返回nil。
③:如果只存在锁,key 不存在,则说明有其他线程获取到了锁(当前线程需要等待),需要返回锁的过期时间。

从上述中就可以看出这个锁是 hash 结构的:

5103209d90e8bc66a6606555a6cce0fe.png

而key的组成应该是:{uuid}:{threadid}
不信?我给你截图...

①:RedissonBaseLock.getLockName(long threadId)

928a6080e7af8191e58bc256dc561dde.png

②:MasterSlaveConnectionManager.MasterSlaveConnectionManager(Config cfg, UUID id)

fdc65ae648b41bda605305f33b0912c5.png

4:获取锁成功

4.1:看门狗

看门狗的存在是为了解决任务没执行完,锁就自动释放了场景。
如默认锁的释放时间为30s,但是任务实际执行时间为35s,那么任务在执行到一半的时候锁就被其他线程给抢占了,这明显不符合需求。
因此就出现了看门狗,专门进行续命操作~~

bdf942b416316cd3a69b24507a96bb1a.png

通过分析底层代码,当锁没有设置自动释放时间才会启用看门狗线程的。
所以我们要预设置过期时间的话最好还是先预估任务的实际执行时间再进行取值为妙...

4.2:时间轮

看门狗的操作实际上就是基于时间轮的。

①:RedissonBaseLock.renewExpiration()

57486225a76e89675492e7e9c82fb78a.png

在此处可以分析到看门狗的执行时间间隔:锁的默认释放时间为30s,因此每10s看门狗就会进行一次续命操作。

79445b5f1f04cef67cab61730466900c.png

9ca97a0c495226371d3e27d03e475fc0.png

42d65a00b75407ad855d87837ed73491.png

上述代码底层点进去后可以看到实际上用了netty的 HashedWheelTimer 类:

②:MasterSlaveConnectionManager.newTimeout(TimerTask task, long delay, TimeUnit unit)

9c2ea9d3995ec620a4a5ebf4ffe327ab.png

功力不够,关于netty的细节就不过多描述了~~

借图说下自己的理解

187fc8ecfe2be0cb89c8b177519d6673.png

如上图为一个时间轮模型,有8个齿轮,指针一秒走一次,那么走完需要8s。

齿轮有两个属性:
task:被执行的任务
bound:当bound = 0 时 task才会被执行,当bound > 0 时,指针每过一次bound - 1 直至为0 。
eg:如果你想31s后执行任务,那么bound应该等于3,齿轮处于第7个位置上。因为3*8+7=31。

4.3:解锁->unlock

底层源码:

f161430279ca2e900b1df557d1f88405.png

①:若当前线程并没有持有锁,则返回nil。
②:当前线程持有锁,则对value-1,拿到-1之后的vlaue。
③:value>0,以毫秒为单位返回剩下的过期时间。(保证可重入)

④:value<=0,则对key进行删除操作,return 1 (方法返回 true)。然后进行redis-pub指令。

redis-pub 之后会被其他获取不到锁的线程给监听到,其他线程又进入下一轮的占锁操作。

5:获取锁失败

5.1:关系类图

这块儿比较麻烦,先给一下比较重要的类图吧...

91faf7f02fe9d3b5b0c410d8a9c3b45f.png

5.2:订阅事件

没获取到锁线程后面在干嘛?当然要持续等待啦...
先在redis中发布订阅消息,等待用完锁的线程通知我~

e00568a93d82e66699945566b46e41be.png

看看订阅主要干了些啥,从源码上分析一波

①:PublishSubscribe.subscribe(String entryName, String channelName)源码:

4b5cac4a7e04ee3eac3708b1f46e766b.png

②:AsyncSemaphore.acquire(Runnable listener)源码:

020a4107c28714673896d7e128487941.png

③:PubSubLock.createEntry() 源码:

b7363c548c4a1229f1bcef76de5ad152.png

④:RedisLockEntry 的部分源码:

ab1f7eded1ebf1de2a0c9ea739490ff5.png

⑤:RedisPubSubConnection.subscribe(Codec codec, ChannelName... channels)源码:

5f82a8521b81d800571a8d34a259c880.png

给张图可能看起来方便点:

2768e803fc9bae14605ecfda97eb4882.png

订阅源码总结:
①:并不是每次每次都会创建RedisLockEntry,理论上是:当前应用内一个channel 对应一个RedisLockEntry 实例。
②:subscribe 的底层是基于netty进行操作的,并不是基于RedisTemplate。
③:不是每次subscribe都会执行到netty层,只有当属于该redis-channel的RedisLockEntry 没有实例化时才会调用到netty层。后续线程的只需要执行RedisLockEntry.acquire 操作即可。

6:redis-pub和redis-sub 是如何遥相呼应的?

6.1:Semaphore.tryAcquire(...)

RedisLockEntry 的latch属性为Semaphore

2737610dcf05c8b6960f9ad863a5183e.png

我们看看RedisLock.lock() 源码:

0a2e744d464c002c5042fcf444ca9c17.png

为什么要用while(true) ?

因为只有一个线程能拿到锁啊,如果第一次拿到的ttl=1433ms,那么线程自旋1433ms就够了,但是因为只能有一个线程拿到锁,所以其他线程要进入下一轮的自旋。

红线区域部分会导致当前线程阻塞。
而每次进行subscirbe后,RedisLockEntry.counter 值就会+1,counter值就代表多少线程正在等待获取锁。

6.2:Semaphore.release()


①:RedisPubSubConnection.onMessage(PubSubMessage message) 方法:

fb9cc5a39851b9f5983d5adba329f6de.png

会调用到下命这个方法 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

②:LockSubPub.onMessage(RedissonLockEntry value, Long message)方法:

a1dc19f8efdff92c2a86d7390d084c15.png

还记得我在上面提到过的吗?下图这个地方

b20cb3c81a55cb433fd4db1b1454dcdb.png

在往下看就是步骤 LockSubPub.onMessage() 的代码了。



https://www.cnblogs.com/zgq7/p/14746128.html

Top Top