>综合>资讯>

redis分布式锁的原理:分布式锁第四篇redisson分布式锁的联锁

时间:2023-12-02 18:21:16/人气:279 ℃

大家好,我是贠学文,点击右上方“关注”,每天为您分享java程序员需要掌握的知识点干货。

凌晨四点的背景

在上一篇文章中,我们通过对redisson分布式锁的源码的剖析,了解了Redisson分布式锁的可重试性和看门狗的实现原理,没有看的朋友请看上一期 那我们今天继续通过对源码的剖析,来看一下redssion分布式锁的联锁、红锁、公平锁、读写的实现原理。

一、redisson分布式锁的联锁和红锁

我们在使用redis做分布式锁的时候,我们为了保证redis的高可用,通常都会做redis的哨兵模式或者主从集群模式,这时如果在某个master节点上创建锁成功,但是还没有来得及同步到slave节点,master就 宕机了,这时候,锁就丢失了。

那么redssion是如何解决这个问题的呢?这就涉及到了redisson中的联锁和红锁。我们先说一下联锁。

1.1 联锁

我们之前在剖析redisson的源码时,看的都是Redissonlock类的源码,但是这个源码,只能处理单锁的情况,即一次性只能为一个锁加锁或者释放锁。而redssion中还提供了RedissonMultiLock类,我们把这个类称之为联锁类,它可以一次性为多个锁加锁或者释放锁。

那RedissonMultiLock类是如何处理联锁的呢?我们先看下它的构造方法的源码,如下图所示:

RedissonMultiLock类构造方法源码

从构造方法中我们可以看出,RedissonMultiLock类是用装饰器模式来实现的联锁。

从前面的文章我们可以知道,redisson为我们提供了很多不同种类的锁,这些不同种类的锁都实现了RLock接口。像RedissonLock、RedissonMultiLock都实现了RLock接口。而RedissonMultiLock类中,又定义了一个存放RLock对象的list属性,然后在构造方法中的参数列中,是一个RLock对象数组,然后在构造方法中,把这个数组转换成了list,add到了list属性里。看到这里我们基本就可以猜到了,RedissonMultiLock在加锁和释放锁时,就是遍历这个list对象,然后依次调用我们传过来的lock对象的对应的加锁和释放锁的方法。这其实就是装饰器模式。

但是我们同时也会想到这样一个问题,它同时对多个锁加锁,如果其中一部分加锁成功了,一部分加锁失败了,这种情况如何处理了,带着这个疑问,我们来看下它的加锁的源码,如下图所示:

RedissonMultiLock类加锁源码

从加锁的源码中我们可以看到,我们之前猜想的没错,它就是对list进行遍历,然后依次调用RLock的tryLock方法。这段代码的重点在于它在加锁之后的处理,它首先判断了一个failedLocksLimit是否等于0,这个变量的是通过调用failedLocksLimit()获得的,它的含义是最多允许失败的数量。每一次遍历加锁失败以后,它都会对failedLocksLimit的值减1,如果failedLocksLimit的值等于0了,就说明它的失败数量已经达到上限了,这时候认为是整体加锁失败,然后对已经加锁成功的,进行解锁,然后返回false。

在RedissonMultiLock中,failedLocksLimit()方法写死返回0,说明一个失败的都不能有,只要有一个加锁失败的,就会认为整体加锁失败。

上面就是RedissonMultiLock类的原理,其实很简单,就是利用装饰器模式,来依次调用通过参数传进来的RLock的对应的方法,然后利用failedLocksLimit()来控制允许失败的数量。

1.1 红锁

红锁对应的类是RedissonRedLock,我们先来看下它的源码,如下图所示:

RedissonRedLock类源码

这个类的源码就很简单了,就是继承了RedissonMultiLock类,然后重写了failedLocksLimit方法,重写后的逻辑,就是超过半数的锁都加锁成功了,就认为是加锁成功了,否则就是加锁失败。

那利用这个特点,我们就可以解决前面提到的锁丢失的问题。我们可以认为redis的各个节点都是互相独立的,没有主从的概念,然后对于每一个节点,都创建一个RedissonLock对象,传递到RedissonRedLock中,然后RedissonRedLock会为这些所有的节点,都尝试加锁,如果超过半数成功的话,就认为加锁成功,否则就加锁失败。这样在加锁成功以后,即使master节点宕机了,其它节点上还有同样的锁,就不会造成锁丢失的问题了。

二、redisson分布式锁的公平锁

redisson中的RedissonFairLock类,提供了公平锁的实现,它实际也是继承了RedissonLock类,然后重写了tryLockInnerAsync方法,利用此方法中的lua脚本,来实现公平锁,我们先来看下lua脚本的源码,如下图所示:

公平锁lua脚本源码一

公平锁的lua脚本源码二

大家可以看到,这段脚本的源码相当的复杂,我就不一一的分析这段源码了,简单说下这点源码的原理:

它是把加锁失败的线程,存放到队列里面,利用队列先进先出的特性,当锁被释放以后,在从队列的头中取出线程,然后利用发布订阅的模式,通知到对应的服务器的对应的线程并唤醒,通知它加锁成功。

那它的问题点在于,我如何精准的通知到某个线程加锁成功呢?在使用非公平锁的时候,所有的线程都是订阅的同一个队列,所以当有消息发布时,会通知到所有线程,无法精准的通知到某一个线程。所以公平锁为了解决这个问题,它是让每一个线程都订阅不同的队列。这样就可以做到精准的通知到某个线程。

但是公平锁的性能比较差,因为它会很频繁的对线程唤醒和挂起,所以用的比较少。而且非公平锁也并不是说永远都不保持公平,在大多数情况下,它还是可以保证先到先得的,只是为了减少对线程的唤醒和挂起的频率,偶尔会有插队的情况。

举个例子来说,假如现在t1-10的10个线程获取同一把锁,然后t1获取锁成功,t2到t10线程在队列中排队,等待被唤醒,这时如果t11线程过来,如果按照公平锁的话,t11线程就会被挂起,并且放到队列的队尾,等待被唤醒。那如果在锁竞争大的时候,就会频繁的对后来的线程做挂起和唤醒,比较消耗性能,这时非公平锁的优势就体现出来了,假如t11线程到来的时候,刚巧t1线程已释放锁,而且t2线程还没有被唤醒,这时t11线程就可以插队,直接获取到锁。减少了对线程挂起和唤醒的次数。

二、redisson分布式锁的读写锁

redisson中的RedissonReadLock类提供了读锁,RedissonWriteLock类提供了写锁,这两个类都是继承了RedissonLock类,然后通过重新tryLockInnerAsync方法和unlockInnerAsync方法,来重写了加锁和释放锁的脚本。我们先来看下源码,如下图所示:

RedissonReadLock类释放锁的lua脚本源码

RedissonReadLock类释放锁的lua脚本源码

RedissonWriteLock类加锁的lua脚本源码

RedissonWriteLock类释放锁的lua脚本源码

从上面脚本的源码可以得出下面结论:

至此,分布式锁,关于分布式锁的篇章,就全部讲解完了。

往期精彩:

作者介绍:

贠学文,具有多年经验的java开发工程师,业余时间利用头条分享技术知识点与自己对技术的感悟,帮助对自己未来感到迷茫的程序员,在技术上得到提升。结识一些志同道合的朋友,相互促进,共同进步。

首页/电脑版/地图
© 2024 CwBaiKe.Com All Rights Reserved.