Redisson(2-2)分布式锁实现对比 VS Java的ReentrantLock之带超时时间的tryLock

Redisson实现了一整套JDK中ReentrantLock的功能,这里对比一下实现的差异和核心的思想。

unfair模式的带超时时间的tryLock(超时时间)

ReentrantLock

这里上来会直接先试下能不能try成功,如果不成功,进入等待并开始竞争等逻辑。

整个锁的核心是通过LockSupport的park方法来实现的,这是调用底层UNSAFE的park方法来实现的。如果在被等待获取的锁释放的时候,该线程会重新被唤醒,然后和其它线程一起竞争,如果没有竞争成功,那么继续park,循环往复,直到获取到锁或者等待超时(这里park的时间是当前要获取锁的线程等待获取锁的剩余时间,当前线程要么因为锁释放被唤醒,要么等到超时)。

这里有个小细节是,如果剩余的等待时间小于spinForTimeoutThreshold这个值,那么就不会再park然后等待唤醒了。这是为了防止park然后唤醒这种线程调度操作,因为剩下的等待时间已经不多,即使一直自旋也不会消耗太多的CPU性能,反而比频繁挂起释放要节省性能,这是一个折衷处理。

RedissonLock

RedissonLock没有LockSupport这种底层级别的工具来帮忙,因为它是分布式的,所以它也需要借助这样一个东西,来实现类似的功能。

我们一般在自己用Redis实现分布式锁的时候,经常设计的操作是轮询Redis去查询该锁的状态,轮询之间会设置一个休眠时间,休眠时间越短,当锁释放的时候响应的就越及时,但是对Redis的压力就越大,因为你单位时间内轮询Redis的次数会增加,所以这是一个痛点。那么如果我们用通知来代替轮询,是不是就可以仿照ReentrantLock那样,通过唤醒操作(借助通知)来唤醒本地的sleep操作,这样就不必定时轮询检查状态了。

而这个功能就要利用Redis的订阅功能,下面看代码:

这里和ReentrantLock一样,先try一下,如果获得了锁就返回,如果没有获得,看下是否已经超过等待获取锁设置的超时时间,如果已经超时,获取锁失败,否则,就要进入等待竞争的过程。

竞争的过程中,第一个操作是subcribe,即上面我们提到的订阅功能。订阅的超时时间即当前要获取锁的线程的剩余等待时间,如果在这个时间范围内没有响应,订阅失败。

订阅的相关操作可以参考:https://my.oschina.net/u/2313177/blog/1925237

“订阅监听Redis消息,并且创建RedissonLockEntry,其中RedissonLockEntry中比较关键的是一个Semaphore属性对象,用来控制本地的锁请求的信号量同步,返回的是netty框架的Future实现”。

如果这个await成功了,代表在等待锁的超时时间之内锁就被释放了,接下来要进入竞态部分了。

这里我理解有两层维度,一层是应用维度,不同的虚拟机之间通过订阅方式竞争锁,某一台业务服务器的java虚拟机会最终成功订阅到这个锁。二层是虚拟机内的线程维度,这里机器内的竞争通过一个共享锁来减少对Redis的压力,因为当前虚拟机的订阅下面挂着多个竞争该锁的不同线程,这些线程中也只有一个会成功获得共享锁,继而再去竞争真正的锁。没有获得共享锁的线程睡眠一段时间,或者被唤醒继续获取锁,或者超时,获取锁失败,而只有获得共享锁成功的线程才有机会和其它虚拟机中同样获取共享锁的线程共同竞争真正的锁。

所以同虚拟机内要竞争真正锁的所有线程先竞争一个共享锁(Semaphore),成功的线程才获取机会和其它虚拟机内同样获取共享锁(Semaphore)的线程竞争真正的锁——跨虚拟机的锁竞争,即分布式锁竞争,这里的关键是共享锁降低了静态,同时又利用订阅机制(通知机制)解决了睡眠时间无法合理设置的问题。

其实https://www.cnblogs.com/ASPNET2008/p/6385249.html文章也说到了这个问题:

所以,如果订阅成功,说明当前机器在虚拟机层面首先抢占到了资源,然后再在当前虚拟机内进行不同线程间的竞争。

像上面说的那样,这里借助信号量来替代了ReentrantLock中的LockSupport的park功能,“通过信号量(共享锁)阻塞,等待解锁消息(这一点设计的非常精妙:减少了其他分布式节点的等待或者空转等无效锁申请的操作,整体提高了性能)。如果剩余时间(ttl)小于wait time ,就在 ttl 时间内,从Entry的信号量获取一个许可(除非被中断或者一直没有可用的许可)。否则就在wait time时间范围内等待”。

这里的共享锁和上面subscribe的是同一把锁。如果本虚拟机的某个线程获取锁之后,当它释放锁的时候,会发布一条取消订阅的消息(这个后面具体会说),这样其它虚拟机才能有平等的机会再次和本虚拟机的其他线程竞争锁,而不是一直在本虚拟机的范围内进行竞争,那样其他虚拟机就会一直处于饥饿状态。

那么什么时候这个订阅的消息会解除当前线程的休眠操作呢?和subscribe对应的当然就是publish了,当执获得锁的线程进行解锁操作的时候(解锁后面会单独说),在lua中会执行‘publish’操作,而publish的消息类型是LockPubSub.unlockMessage,这个消息会触发订阅消息的共享锁(Semaphore)的唤醒操作,然后发起对该锁的新一轮竞争。

而最后无论获取锁成功与否,都要解除当前线程对该锁的订阅消息。

关于等待时间(Semaphore的休眠时间),上面ReentrantLock的park的时间是当前线程获取锁的等待剩余时间。这里本地等待锁的睡眠时间略有不同,使用的是Redis中锁的生命周期剩余时间,当然在这个睡眠过程中,可能会因为锁释放而唤醒,因为有对当前锁的订阅操作。

猜你喜欢

转载自blog.csdn.net/xxcupid/article/details/88242742