Zookeeper分布式锁实现Curator十一问

前面我们通过Redis分布式锁实现Redisson 15问文章剖析了Redisson的源码,理清了Redisson是如何实现的分布式锁和一些其它的特性。这篇文章就来接着剖析Zookeeper分布式锁的实现框架Curator的源码,看看Curator是如何实现Zookeeper分布式锁的,以及它提供的哪些其它的特性。

Curator框架是封装对于zk操作的api,其中就包括了对分布式锁的实现,当然Curator框架也包括其它的功能,分布式锁只是Curator的一部分功能。

本文的目录跟Redisson的目录比较相似,主要是为了方便大家对比redis和zk分布式锁的实现。

一、ZK分布式锁实现原理

实现Zookeeper分布式锁,主要是基于Zookeeper的临时顺序节点来实现的。

当客户端来加锁的时候,会先在加锁的节点下建立一个子节点,这个节点就有一个序号,类似 lock-000001 ,创建成功之后会返回给客户端所创建的节点,然后客户端会去获取这个加锁节点下的所有客户端创建的子节点,当然也包括自己创建的子节点。拿到所有节点之后,给这些节点进行排序,然后判断自己创建的节点在这些节点中是否排在第一位,如果是的话,那么就代表当前客户端就算加锁成功了,如果不是的话,那么就代表当前客户端加锁失败。

加锁失败的节点并不会不停地循环去尝试加锁,而是在自己创建节点的前一个节点上加一个监听器,然后就进行等待。当前面一个节点释放了锁,就会反过来通知等待的客户端,然后客户端就加锁成功了。

为什么需要在前一个节点加个监听器?

假设有很多客户端来加锁,然后加锁失败的都对前一个节点加一个监听。那么一旦第一个加锁成功的客户端线程释放了锁,那么被唤醒的就是第二个客户端线程,第二个客户端线程就会加锁成功,执行完任务之后就释放了锁,那么就会唤醒第三个客户端线程,第三个客户端线程加锁成功,执行完任务之后就释放了锁,唤醒第四个客户端线程,以此类推,所以每次释放锁都会唤醒下一个节点,这样每个加锁的线程都会加锁成功,所以监听器的作用是唤醒加锁失败阻塞等待的客户端。

二、为什么使用临时顺序节点

下面介绍一下临时节点、持久化节点、顺序节点的特性。

1)临时节点

临时节点,指的是节点创建后,如果创建节点的客户端和 Zookeeper 服务端的会话失效(例如断开连接),那么节点就会被删除。

2)持久化节点

持久化节点指的是节点创建后,即使创建节点的客户端和 Zookeeper 服务端的会话失效(例如断开连接),节点也不会被删除,只有客户端主动发起删除节点的请求,节点才会被删除。

3)有序节点

有序节点,这种节点在创建时会有一个序号,这个序号是自增的。有序节点既可以是有序临时节点,也可以是有序持久化节点。

从上面节点的特性可以知道,临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,如果使用临时节点的话,如果客户端发生异常的话,没有来得及主动释放锁,就能避免锁无法释放导致死锁的情况。因为一旦客户端异常,那么客户端和服务端之间的会话就会失效,然后临时节点就会被删除,这样就释放了锁;而持久化节点在由于会话失效无法被删除,那么就不会去释放锁,这样就会产生死锁的问题。

从这里可以看出redis和zk防止死锁的实现是不同的,redis是通过过期时间来防止死锁,而zk是通过临时节点来防止死锁的。

为什么使用顺序节点?其实为了防止羊群效应。如果没有使用顺序节点,假设很多客户端都会去加锁,那么加锁就会都失败,都会对加锁的节点加个监听器,那么一旦锁释放,那么所有的加锁客户端都会被唤醒来加锁,那么一瞬间就会造成很多加锁的请求,增加服务端的压力。

所以综上,临时顺序节点是个比较好的选择。

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

前面关于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的循环中。

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

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

boolean acquire(long time, TimeUnit unit) throws Exception

这个方法相比不指定等待时间的方法最主要的区别就是加锁失败之后,调用的阻塞的方法不一样。当不指定超时时间就会调用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分布式锁是个不错的选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/173067.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenMMlab导出yolox模型并用onnxruntime和tensorrt推理

导出onnx文件 直接使用脚本 import torch from mmdet.apis import init_detector, inference_detectorconfig_file ./configs/yolox/yolox_tiny_8xb8-300e_coco.py checkpoint_file yolox_tiny_8x8_300e_coco_20211124_171234-b4047906.pth model init_detector(config_fi…

MATLAB中corrcoef函数用法

目录 语法 说明 示例 矩阵的随机列 两个随机变量 矩阵的 P 值 相关性边界 NaN 值 corrcoef函数的功能是返回数据的相关系数。 语法 R corrcoef(A) R corrcoef(A,B) [R,P] corrcoef(___) [R,P,RL,RU] corrcoef(___) ___ corrcoef(___,Name,Value) 说明 R corrc…

sebp/elk镜像历史版本

最近因为之前sebp/elk的镜像和容器出现问题而误删了. 新版本随便功能全, 但是配置较为繁琐. 因此想要根据之前的截图找到之前的版本. 但是查看版本需要科学. 因此在花时间研究出来科学的方法之后, 还是决定将历史的版本信息留存下来, 以供后续开发需要 相关官网(需科学上网): s…

【洛谷算法题】P5715-三位数排序【入门2分支结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5715-三位数排序【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格式…

【Seata源码学习 】篇五 注册分支事务

【Seata源码学习 】篇五 分支事务注册 1.远程服务调用绑定XID 回到事务模版方法类TransactionalTemplate中 beginTransaction(txInfo, tx);Object rs;try {// Do Your Business// 执行执行拦截器链路rs business.execute();} catch (Throwable ex) {// 3. The needed busine…

基于Java SSM框架+Vue留学生交流互动论坛网站项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架Vue实现学生交流互动论坛网站演示 摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所…

【腾讯云 HAI域探秘】基于高性能应用服务器HAI部署的 ChatGLM2-6B模型,我开发了AI办公助手,公司行政小姐姐用了都说好!

目录 前言 一、腾讯云HAI介绍&#xff1a; 1、即插即用 轻松上手 2、横向对比 青出于蓝 3、多种高性能应用部署场景 二、腾讯云HAI一键部署并使用ChatGLM2-6B快速实现开发者所需的相关API服务 1、登录 高性能应用服务 HAI 控制台 2、点击 新建 选择 AI模型&#xff0c;…

【C/C++】如何不使用 sizeof 求数据类型占用的字节数

实现代码&#xff1a; #include <stdio.h>#define GET_TYPE_SIZE(TYPE) ((char *)(&TYPE 1) - (char *) & TYPE)int main(void) {char a a;short b 0;int c 0;long d 0;long long e 0;float f 0.0;double g 0.0;long double h 0.0;char* i NULL;print…

如何在gitlab上使用hooks

参考链接&#xff1a;gitlab git hooks 1. Git Hook 介绍 与许多其他版本控制系统一样&#xff0c;Git 有一种方法可以在发生某些重要操作时&#xff0c;触发自定义脚本&#xff0c;即 Git Hook&#xff08;Git 钩子&#xff09;。 当我们初始化一个项目之后&#xff0c;.git…

Qt 样式表

QLabel&#xff0c;应用于Widget&#xff1a; .QLabel {background-color:pink; }.QLabel[warnlevel_1] {border:5px solid yellow; }.QLabel[warnlevel_2] {border:5px solid red; } QWidget{background-color:rgb(54,54,54); }QLineEdit{border: 1px solid #ABCDA0; /…

Android设计模式--桥接模式

闻正言&#xff0c;行正道&#xff0c;左右前后皆正人 一&#xff0c;定义 将抽象部分与实现部分分离&#xff0c;使它们都可以独立地进行变化 二&#xff0c;使用场景 从模式的定义中&#xff0c;我们大致可以了解到&#xff0c;这里的桥接的作用其实就是连接抽象部分与实现…

PHP 针对mysql 自动生成数据字典

PHP 针对mysql 自动生成数据字典 确保php 可以正常使用mysqli 扩展 这里还需要注意 数据库密码 如果密码中有特殊字符 如&#xff1a; 首先&#xff0c;我们需要了解MySQL中的特殊字符包括哪些。MySQL中的特殊字符主要包括以下几类&#xff1a; 1. 单引号&#xff08;&a…

嵌入式硬件电路·电平

目录 1. 电平的概念 1.1 高电平 1.2 低电平 2. 电平的使用场景 2.1 高电平使能 2.2 低电平使能 2.3 失能 1. 电平的概念 电平是指电信号电压的大小或高低状态。在数字电子学中&#xff0c;电平有两种状态&#xff0c;高电平和低电平&#xff0c;用来表示二进制中…

ChatGPT初体验:注册、API Key获取与ChatAPI调用详解

自从2022年10月&#xff0c;ChatGPT诞生以后&#xff0c;实际上已经改变了很多&#xff01;其火爆程度简直超乎想象&#xff0c;一周的时间用户过百万&#xff0c;两个月的时间用户过亿。 目前ChatGPT4已经把2023年4月以前的人类的知识都学习到了&#xff0c;在软件工程里面&am…

Matplotlib不规则子图_Python数据分析与可视化

除了网格子图&#xff0c;matplotlib还支持不规则的多行多列子图网格。 plt.GridSpec()对象本事不能直接创建一个图形&#xff0c;他只是 plt.subplot()命令可以识别的简易接口。 这里创建了一个带行列间距的23网格&#xff1a; grid plt.GridSpec(2, 3, wspace0.4, hspace0…

mac VScode 添加PHP debug

在VScode里面添加PHP Debug 插件,根据debug描述内容操作 1: 随意在index里面写个方法,然后用浏览器访问你的hello 方法,正常会进入下边的内容 class IndexController {public function index(){return 您好&#xff01;这是一个[api]示例应用;}public function hello() {phpin…

group by

引入 日常开发中&#xff0c;我们经常会使用到group by。你是否知道group by的工作原理呢&#xff1f;group by和having有什么区别呢&#xff1f;group by的优化思路是怎样的呢&#xff1f;使用group by有哪些需要注意的问题呢&#xff1f; 使用group by的简单例子group by 工…

java学习part12多态

99-面向对象(进阶)-面向对象的特征三&#xff1a;多态性_哔哩哔哩_bilibili 1.多态&#xff08;仅限方法&#xff09; 父类引用指向子类对象。 调用重写的方法&#xff0c;就会执行子类重写的方法。 编译看引用表面类型&#xff0c;执行看实际变量类型。 2.父子同名属性是否…

数组与链表

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 除了HashMap&#xff0…

计算机服务器中了mallox勒索病毒如何处理,mallox勒索病毒解密文件恢复

科技技术的发展推动了企业的生产运营&#xff0c;网络技术的不断应用&#xff0c;极大地方便了企业日常生产生活&#xff0c;但网络毕竟是一把双刃剑&#xff0c;网络安全威胁一直存在&#xff0c;近期&#xff0c;云天数据恢复中心接到很多企业的求助&#xff0c;企业的计算机…