Zookeeper分布式锁实现Curator

一、加锁的逻辑是如何实现的

前面关于ZK分布式锁实现原理已经说过了,接下来就来看一下代码的实现。

加锁的使用方法如下,接下来几节会着重讲解这段代码背后的逻辑

acquire方法的实现

acquire方法会去调用internalLock方法,传入超时时间 -1 和单位 null,也就代表了如果加锁不成功会一直阻塞直至加锁成功,不会超时。

internalLock方法会先去获取当前线程,然后从threadData中获取当前线程对应的LockData,这里面封装了加锁的信息和次数,是实现可重入锁的关键,当然第一次加锁这里肯定是没有的,会继续下走 internals.attemptLock 加锁。

attemptLock方法

先通过driver的createsTheLock去创建节点。

从这里看出,创建的节点类型是临时顺序节点,创建成功之后,就会返回当前创建的节点。

节点创建成功之后,会调用internalLockLoop方法来加锁。

通过getSortedChildren方法获取排好序的子节点,然后获取当前的节点名称,再通过 driver.getsTheLock判断当前的节点有没有加锁成功,返回一个PredicateResults判断的结果,这里面存的就是否加锁成功的信息。

第一次加锁,那么到这里就加锁成功了。之后就会封装一个LockData对象,放入threadData 的map中。

加锁的流程如下图:

二、如何实现可重入加锁

上文加锁的时候提到了,当第一次加锁成功之后,会往threadData放入该加锁的线程对应的LockData。

LockData主要封装了当前线程、加锁的次数、加锁的节点。

此时如果第二次来加锁,那么就会从threadData中获取到加锁的信息,然后将加锁次数加1,就代表了加锁成功,然后直接返回。

所以可重入加锁的实现很简单,就是在客户端中判断有没有加过锁,加过的话就将加锁次数累加1,压根就跟服务端没有交互。

注意Redisson可重入加锁的实现跟的Curator是不一样的,Redisson的加锁次数是存在Redis的服务端的,而Curator是存在客户端的。

三、加锁失败之后如何实现阻塞等待加锁

前面加锁的逻辑主要是说了加锁成功的情况,这里就来说一下加锁失败的情况。

继续来看internalLockLoop方法。

前面说过,判断有没有加锁成功,会返回一个PredicateResults,这里面包含了有没有加锁成功的信息,同时如果没有加锁成功,就会返回需要监听的节点,也就是当前创建的节点的前一个节点。

所以没有加锁成功,就会走else的逻辑,对上一个节点加一个监听器 watcher

然后就会调用 wait 方法,进行等待。

当前一个节点被删除了,也就是释放了锁,那么就会回调这个监听器watcher的方法。

所以,这个watcher的作用就是调用notifyAll方法唤醒调用wait方法的线程,这样线程就会继续尝试加锁,因为是在一个while的循环中。

四、如何实现阻塞等待一定时间还未加锁成功就放弃加锁

可通过下面这个方法来实现实现阻塞等待一定时间还未加锁成功就放弃加锁。

这个方法相比不指定等待时间的方法最主要的区别就是加锁失败之后,调用的阻塞的方法不一样。当不指定超时时间就会调用wait()方法,不会传入等待时间,不被唤醒就会一直阻塞;指定超时时间的时候,就会调用wait(long timeout)指定等待的时间,这样如果等待时间一到,线程就会醒过来,然后再次尝试加锁,一旦加锁失败,就会放弃加锁。

七、如何主动释放锁和避免其它线程释放锁

释放锁release方法

释放锁其实很简单,就是拿出当前线程对应的LockData,如果没有,就说明当前线程没有加过锁,就会抛出异常,所以Curator就是通过这个判断来防止其它线程释放了自己线程加的锁。

如果加锁了,那么LockData就不会为null,然后将加锁次数递减1,得到newLockCount,代表了剩下的加锁次数。

  • 如果newLockCount > 0,说明锁没释放完,有可重入加锁,然后什么事都不干,直接返回了。

  • 如果newLockCount < 0,就抛异常,但是一般不会出现。

  • 剩下的一种情况就是newLockCount == 0 ,说明锁已经完完全全释放完了,然后通过internals.releaseLock删除加锁的节点。

服务端删除节点之后,就会通知监听该节点的客户端,然后客户端就会回调watcher监听器,唤醒阻塞等待的线程,线程被唤醒后再进行一次判断就能加锁成功。

到这里,就讲完了加锁和释放锁的过程,整个加锁和释放锁的过程就如下图所示。

五、如何实现公平锁

其实使用临时顺序节点实现的分布式锁就是公平锁。所谓的公平锁就是加锁的顺序跟成功加锁的顺序是一样的。

因为节点的顺序就是被唤醒的顺序,所以也就是加锁的顺序,所以天生就是公平锁。

六、如何实现读写锁

读写锁使用如下。

创建节点的时候,节点的内容中会有一个标记来代表当前节点加的是什么类型的锁。

当需要加写锁时,需要判断自己创建的节点是否排在第一位,如果是就能加锁成功,所以一旦前面有节点,不论前面加是读锁还是写锁,那么都是加锁失败,实现了读写互斥和写写互斥。当然写锁和读锁都是可以重入加锁的。

当需要加读锁的时候,会去判断自己创建节点的前面有没有写锁,如果没写锁,那么说明前面加的都是读锁,那么读锁就能加锁成功,读读不互斥,如果前面有写锁,那么就加锁失败(自己加的写锁除外),读写互斥。

七、如何实现批量加锁

批量加锁的意思就是同时加几个锁,只有这些锁都算加成功了,才是真正的加锁成功。

Redisson也实现了批量加锁的功能,Redisson的实现通过RedissonMultiLock类实现的,RedissonMultiLock会去遍历需要加的锁,然后每个都加成功之后才算加锁成功。Curator是封装了InterProcessMultiLock类来实现的批量加锁的,那么InterProcessMultiLock如何实现的呢?

使用代码如下。

InterProcessMultiLock的acquire的方法实现。

从这里可以看出,InterProcessMultiLock也是遍历传入的锁,然后每个锁都加锁成功了,InterProcessMultiLock才算加锁成功。

所以从这里可以看出,跟Redisson实现的批量加锁的实现思想上基本是一样的,都是遍历加锁。

八、ZK分布式锁和Redis分布式锁到底该选谁

redis分布式锁:

  • 优点:性能高,能保证AP,保证其高可用,

  • 缺点:正如Redisson的那篇文章所言,主要是如果出现主节点宕机,从节点还未来得及同步主节点的加锁信息,可能会导致重复加锁。虽然Redis官网提供了RedLock算法来解决这个问题,Redisson也实现了,但是RedLock算法其实本身是有一定的争议的,有大佬质疑该算法的可靠性;同时因为需要的机器过多,也会浪费资源,所以RedLock也不推荐使用。

zk分布式锁:

  • 优点:zk本身其实就是CP的,能够保证加锁数据的一致性。每个节点的创建都会同时写入leader和follwer节点,半数以上写入成功才返回,如果leader节点挂了之后选举的流程会优先选举zxid(事务Id)最大的节点,就是选数据最全的,又因为半数写入的机制这样就不会导致丢数据

  • 缺点:性能没有redis高

所以通过上面的对比可以看出,redis分布式锁和zk分布式锁的侧重点是不同的,这是redis和zk本身的定位决定的,redis分布式锁侧重高性能,zk分布式锁侧重高可靠性。所以一般项目中redis分布式锁和zk分布式锁的选择,是基于业务来决定的。如果你的业务需要保证加锁的可靠性,不能出错,那么zk分布式锁就比较符合你的要求;如果你的业务对于加锁的可靠性没有那么高的要求,那么redis分布式锁是个不错的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值