万剑归宗之七剑下天山,redisson的百锁解构(下)
上文,我们分析了redisson剑谱里面的前四式,参见【万剑归宗,redisson的百锁解构(上) 】,本文将继续解构后面的四个招式,对于这些招式的拆解,除了可以让我们对redisson知其然知其所以然,也可以帮助我们去理解jdk中本身就已经实现的那些锁。继续演练余下的几招,以指为剑,剑破苍穹。
【 招式四 读写锁 】
读写锁,在很多业务场景中,读多写少,多个客户端可以同时读,但是有人写的时候不能读,这个时候读写锁就是最佳选择了,自然是要分为读锁,写锁,而且肯定都是要单独加锁的,这其中就涉及了锁互斥的机制,读读,读写,写写,写读,这些情况都是怎么处理的呢?OK,我们从源码入手,见招拆招。
【 读锁&加锁】
读锁的加锁逻辑,也是在lua脚本中的,这段脚本也不长,首先呢他是根据
锁key的hash结构中去hget一个mode的值,这个mode的值要么是读read,要么是写write,我们第一次来加读锁,那肯定就是不存在的,就进入分支进行加锁,加锁的逻辑很简单,把mode设置为read,设置37f75873-494a-439c-a0ed-f102bc2f3204:1,值为1,设置{anyLock}:37f75873-494a-439c-a0ed-f102bc2f3204:1:rwlock_timeout:1,
值为1,并且将上面俩个俩个值的有效期设置为30000毫秒,就返回nil啦,加锁成功。
【 读锁&读读&读写 】
那么读读会互斥吗?从上面的源码中我们可以看出来,并不会,如果已经有一个客户端已经加锁了,那么此时的mode还是read,会进入到判断分支,hincrby自增新的锁key [ID:threadId],这里这么做,有俩个好处,如果是同一个客户端同一个线程,就会自增1,如果不是的话,就会新增一个kv,[ID2:threadId2],也会和之前一样拼接一个rwlock_timeout的kv,,并且给所有的key设置有效期为30000毫秒。到这里,很清晰了,读锁的读读是不会互斥的。
那么读写会互斥吗?假设我们已经加了一把读锁,此时有人过来加写锁呢?这样,我们先来分析分析写锁的加锁机制,再来回答这个问题。【结论:读读不互斥,读写互斥】
【 写锁&加锁 】
写锁的逻辑是怎样的呢?
首先还是先获取了当前的mode是什么,我们先假设第一次加写锁,此时还是直接进入分支,设置模式为write,设置[ID:threadId1:write]为1,最后再将锁key的有效期设置为30000毫秒;好啦,这里我们来看看,读写是否互斥,此时假设已经有人加了读锁,此时来加写锁,直接就是闭门羹,返回了ttl的剩余有效期,那么客户端就会进入循环去尝试获取锁,【结论:读写是互斥的】
【 写锁&写读 】
当一个客户端已经加了写锁之后,这个时候新的客户端来加读锁,这里就要看情况了,因为看代码中会有所区分,如果是当前持有写锁的客户端的当前线程来加读锁,是没问题的,如果不是,就直接返回剩余时间,一直循环等待加锁了,【结论:写读看情况,如果是持有写锁的客户端线程加读锁,ok,否则互斥】
【 写锁&写写 】
如果是写写呢?此时已经有用户加了写锁,这时,一个新客户端来尝试加写锁,此时也会进行判断,如果是当前持有写锁的客户端线程,那么就会进入分支,也是操作一些自增,延长时间的操作,如果不是呢,就会返回剩余时间,进入等待循环尝试加锁了。【结论:写写互斥,但是可重入】
【 读锁&watchdog机制&可重入 】
redisson一般加锁的watchdog机制我们都已经分析过了,请看【汪~汪~汪~redisson的WatchDog是如何看家护院的? 】,而读锁的watchdog有一些特殊的逻辑,这里我们也来看看,首先显示获取了这个锁key对应的hash结构中是否存在加锁,如果存在,就会先将锁key的有效期延长为30000毫秒,接着hlen查看hash结构中数量,如果大于1就进入分支,获取所有key,并且进行遍历,获取到值为number的key,其实就是为了拼接得出{anyLock}:37f75873-494a-439c-a0ed-f102bc2f3204:1:rwlock_timeout:1
所有这样的key,并且将这些可以的时间都延长,这里我们也可以看出来,读锁是可重入的,然后就返回继续定时续期锁key的有效期。
【读锁 &释放】
接下来我们继续看看读锁的释放逻辑,这个脚本略长,我们来细细分析一波:释放逻辑呢,主要分成几种情况,我们一一来看。
首先,如果mode值不存在那么也就直接返回1了,先判断一波当前客户端线程是否存在加读锁,如果不存在,说明有问题,返回nil;接下来也说明了读锁存在,先对读锁的值-1,返回了当前的counter值,如果等于0,就直接把锁key删除hdel,紧接着我们就去删除rwlock_timeout对应的(counter+1)key删除;接下来还是去判断hash机构中的key的数量,
情况1:如果大于1,去遍历所有的key,遍历所有的rwlock_timeout,并且pttl获取他们的剩余时间,并且拿到了这些key值中最大的有效期时间maxRemainTime,如果大于0,就将锁key的有效期设置为maxRemainTime,并且返回0 ,这时候释放锁不成功。如果是写模式呢,直接返回0;
情况2:如果小于1,就直接删除锁key,释放锁成功。
对于读锁的释放,因为存在可重入性,所以逻辑上要进行循环判断,只有全部都释放了,读锁才会真正释放了。
【 写锁&释放锁 】
写锁的释放,写锁也是存在可重入的,当model是write的时候,
先判断当前线程的写锁是否存在,如果不存在,返回nil;否则,也是先对写锁值-1,如果值大于0,就返回0,释放锁不成功;否则,就开始进行删除操作,将写锁对应的值都删除,还有个小细节,这里回去判断hlen key的个数,如果等于1,说明没有客户端持有锁,删除锁key,否则说明有读锁持有,就要将mode模式转为read;返回1,释放锁成功。
【招式五 信号量】
信号量,Semaphore,这种锁机制很特殊,在很多特殊场景也会得到运用,他可以指定允许获取锁的线程个数,允许指定数量的多个客户端线程获取同一把锁,一旦其中某个线程释放了,其他线程也可以获得这把锁。他的实现原理是怎么样的呢?
我们在使用api加锁的时候会传入permits,允许加锁的客户端线程个数,这里加锁的逻辑并不复杂,第一次加锁,判断当前key对应的数量【value】和【permits=1】的大小,如果大于直接进入分支,对当前锁key的值-1,返回1,加锁成功,可以想象,一旦经过多次-1,value=0的时候,0>=1不成立,就会返回0,这个客户端线程就需要循环等待尝试加锁了。
释放锁的逻辑更简单,每次释放锁,就对锁key的value进行+1,这样子,新的客户端加锁就能获取成功。so easy~~
【招式六 CountDownLatch 闭锁】
这也是一把好玩的锁,使用此锁,需要指定数量的客户端线程都加锁,否则其他加锁的线程会阻塞住,直到所有的客户端都加锁成功才会进行往下的逻辑,什么样的场景适用呢,如果我们某个业务操作需要多个步骤都完成才能进行下一个步骤,那么就可以用这个锁啦,直接锁住,等待所有任务完成,释放锁之后,就可以进行下一步操作了
那么他是怎么实现的呢?
首先肯定是设置数量trySetCount(count),很简单,就是对key设置了
数量n,紧接着就需要n个线程去操作countdown(),这其中的逻辑也很简单,就是每次操作就会将key的值-1,直到key的值为0的时候,就会删除掉这个key,
而其他地方会有一个操作,就是await(),他会不断循环获取key的值,直到值等于0才会往下操作,是不是很简单,就实现了这一机制。
【 招式七 可重入非公平锁 】
可重入非公平锁的机制我们在前面的篇文章中已经讲解过了,参见【扒开Redisson的小棉袄,Debug深入剖析分布式锁之可重入锁No.1】,回顾一下吧。
对于redisson分布式锁框架的解构到这里就差不多了,是不是觉得简单一招一式,也成就了分布式锁凌厉的剑气,对于上下俩篇的解构,其中涉及了七种分布式锁的机制,若文中有描述错误的,欢迎小伙伴指正批评,悉心接受。谢谢大家观看。