浅谈Zookeeper客户端库Curator实现加锁的原理

  • 一、非公平锁(互斥)
  • 二、公平锁(互斥)
    • 1、概念
    • 2、源码
    • 3、补充
  • 三、共享锁
    • 1、概念
    • 2、源码

一、非公平锁(互斥)

一想到加锁,我们自然就能够想到一个基本的锁机制,如下。

下图是一个非公平锁的模型,其目的是为了提高并发的。但在Zookeeper中,非公平并不一定就会提高对应的性能,所以并非完全使用这个模型(curator使用的是公平锁模型),但是大体的流程差不多

以上加锁逻辑为,对一个请求进行加锁,如果对应的加锁对象没有锁,就去创建一个节点,如果节点创建成功,则表示获取锁成功。在没有释放锁的时候,第二个请求会去判断是否有其他对象,此时判断为是,则该节点会进行阻塞监听。直到第一个请求释放锁之后,就会触发对应的监听机制通知等待的请求,第二个请求再次进行获取锁的判断操作。

如上实现方式在并发问题比较严重的情况下,性能会下降的比较厉害,主要原因是,所有的连接都在对同一个节点进行监听,当服务器检测到删除事件时,要通知所有的连接,所有的连接同时收到事件,再次并发竞争,这就是惊群效应。这种加锁方式是非公平的

想要避免惊群效应带来的性能损耗,可以使用公平锁的机制对其优化,提高响应性能。


二、公平锁(互斥)

使用了公平锁,获取锁的情况就根据请求先后进行决定,使用的样例代码如下

@PostMapping("/stock/deduct")
public Object reduceStock(Integer id) throws Exception {InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/product_" + id);try {// 加锁interProcessMutex.acquire();// 业务逻辑orderService.reduceStock(id);} catch (Exception e) {if (e instanceof RuntimeException) {throw e;}}finally {// 释放锁interProcessMutex.release();}return "ok:" + port;
}

1、概念

主体流程:

  1. 与上面的非公平锁的逻辑大体相同
  2. 主要区别为将获取锁对象和创建节点两个步骤合二为一了,并且添加了一些逻辑判断来筛选出具体的获取锁的节点
  3. 还有一个区别是,对应的监听机制不再是对一个节点进行监听,而是对他的上一个节点进行监听

如上借助于临时顺序节点,节点顺序监听,可以避免同时多个节点的并发竞争锁,缓解了服务端压力。这种实现方式所有加锁请求都进行排队加锁,是公平锁的具体实现。

2、源码

1、初始化基础配置,包含对应的zookeeper客户端、未来使用的加锁目录,以及加锁驱动类

public InterProcessMutex(CuratorFramework client, String path)
{this(client, path, new StandardLockInternalsDriver());
}

2、处理可重入锁逻辑

第一步:如果能获取到数据,表示曾经完成过加锁逻辑,如今再次加锁,直接使用可重入锁逻辑即可。即直接加锁数量+1,然后返回;

第二步:进行加锁逻辑;

第三步:如果加锁成功,会返回加锁成功的路径,将其封装为一个lockData对象,再放进本地缓存Map。未来第一步就能直接获取到。

3、attemptLock方法中核心逻辑在createsTheLock和internalLockLoop两个方法中

  1. createsTheLock:创建对应的节点。这里的driver对象,就是初始化的时候传入的对象
  2. internalLockLoop:对该节点进行逻辑判断(能加锁成功,怎么走;不能加锁成功,又怎么走)

4、createsTheLock

熟悉的链式编程风格,又是一个构建者设计模式,核心方法可直接看forPath方法。

该方法不难看出,是想在zookeeper配置中心的定制路径下面,创建一个目录节点,然后返回对应的路径。

这里创建的是Zookeeper中的container节点,该节点的特性是如果该节点下面没有文件,该节点会自动删除。

5、internalLockLoop:进行加锁判断

该方法核心逻辑也只有两步:

  1. getSortedChildren:获取当前路径下面所有子节点,并且完成排序
  2. getsTheLock:将排好序的子节点和上一步创建的节点进行判断,完成相关逻辑
  3. 最终选择返回加锁成功的节点,还是完成对节点进行相关的阻塞监听

6、具体判断

  1. 获取加锁返回节点得索引下标
  2. 判断我们加锁得节点是不是小于maxLeases(在初始化InterProcessMutex对象得时候,给出得默认值1)
  3. 如果加锁节点小于1(只有索引位置为1才会小于1),表示我们创建的节点是最小节点,0<1就会判定为true,继而第二步为null,最终返回给一个封装了null-true节点信息的加锁对象(后续为haveTheLock=true,不需要监听任何节点)
  4. 如果加锁节点大于1,表示我们创建的节点不是最小节点,对应的判定为flase,对应的pathToWatch表示对其前一个节点进行监听,然后返回这个封装了监听的对象(后续为调用usingWatcher方法,对基础路径+节点名称添加监听,根据对应的阻塞时间,阻塞等待wait方法)
  5. 上面两个括号里面的后续为,为5大步(上一步)的判断逻辑。

总结

上诉源码流程可以简单描述为:

  1. 首先会根据当前线程的加锁情况,判断是否是重入锁
  2. 然后会在zookeeper上根据对应的参数配置,创建一个临时的顺序节点
  3. 获取到返回的顺序节点,判断它是否为当前路径下最小的节点
  4. 如果是,则获取到锁
  5. 如果不是则完成对其前一个节点的监听
  6. 等某个节点监听的节点释放锁之后,会唤醒该节点,继而达到一条链的形式

3、补充

幽灵节点,以之前的图示为例。

当我们在创建01节点的时候,节点创建成功,但是返回给前台的时候网络波动,导致未能成功返回对应的节点,curator会触发对应的重试机制,会直接创建一个新的顺序节点02节点。这就会导致,未来02这个节点会对01节点完成监听,但是01节点客户端是不知道的,就会导致无法监控到01节点,最终出现01节点一直存活,这就是01节点就称为幽灵节点。

解决方式:通过 Protection模式能够避免这个问题,

1、在每次添加节点时为顺序节点拼接一个uuid的前缀

2、如果加锁失败,就会触发curator的重试机制

3、第二次加锁,会带上第一次相同的uuid

4、在创建顺序节点的时候会先判断,对应的含有uuid的节点是否存在

5、如果存在就表示曾经是创建成功的,直接返回。如果不存在就创建,然后返回。

前面这两种加锁方式有一个共同的特质,就是都是互斥锁,同一时间只能有一个请求占用,如果是大量的并发上来,性能是会急剧下降的,所有的请求都得加锁,那是不是真的所有的请求都需要加锁呢?答案是否定的,比如如果数据没有进行任何修改的话,是不需要加锁的,但是如果读数据的请求还没读完,这个时候来了一个写请求,怎么办呢?有人已经在读数据了,这个时候是不能写数据的,不然数据就不正确了。直到前面读锁全部释放掉以后,写请求才能执行,所以需要给这个读请求加一个标识(读锁),让写请求知道,这个时候是不能修改数据的。不然数据就不一致了。如果已经有人在写数据了,再来一个请求写数据,也是不允许的,这样也会导致数据的不一致,所以所有的写请求,都需要加一个写锁,是为了避免同时对共享数据进行写操作。


三、共享锁

在很多并发锁中都会有共享锁的实现方案,如AQS下的ReentrantReadWriteLock锁,又比如Redisson的RedissonReadWriteLock锁,这些都是在原先互斥锁的基础上进行优化,细化锁的粒度,演化出的读写锁。

1、概念

read请求,如果前面都是读锁,那就直接获取锁。如果read前面有写锁,则该请求无法直接获取锁,而是完成对离它最近的一个写锁添加监听。

如下图中,01、02节点能够直接获取到锁,并且不需要添加任何监听。03节点是一个写锁,无法直接获取锁,完成对其上一个节点的监听(类比第二节中的公平锁了逻辑)。04节点是一个读请求,会对离它最近的一个写请求进行监听,即对03节点进行监听。同理05是写请求,也监听03节点…

2、源码

1、读锁入口

上面为写锁、下面为读锁。写锁和上面的互斥共享锁逻辑相同

maxLeases:Integer.MAX_VALUE(区别于写锁的1)

读写锁分别会带上自己的标记READ_LOCK_NAME、WRITE_LOCK_NAME

2、进行加锁判断

  1. 遍历对应加锁目录下的所有子节点。node是排好顺序的,if会依次获取到写锁的索引下标位置。index表示当前遍历的索引下标,firstWriteIndex表示遍历过程中最近的一个写锁的位置。这个min最小判断就能够获取到离当前下标最近的一个写锁的位置
  2. 直到找到我们自己的节点就跳出循环
  3. 这里和前面的判断逻辑大致相同
  4. 判断当前节点的下标是不是小于在它前面,并且离它最近的一个写锁的位置,即它前面有没有写锁
  5. 如果前面没有写锁,就会为false。比如1为当前节点,写锁位置为4。那么ourIndex为1,1小于Integer.MAX_VALUE为true
  6. 反之,如果写锁为1,自己节点为4,firstWriteIndex就为1,ourIndex为4,对应的getsTheLock为false,然后封装一个对firstWriteIndex位置(即1节点)的监听给当前对象

浅谈Zookeeper客户端库Curator实现加锁的原理相关推荐

  1. zookeeper客户端库curator分析

    zookeeper客户端库curator分析 前言 综述 zookeeper保证 理解zookeeper的顺序一致性 之前使用zookeeper客户端踩到的坑 curator 连接保证 连接状态监控以 ...

  2. 浅谈zookeeper

    文章目录 浅谈Zookeeper 一.角色介绍 1.Leader:领导者 2.Follower:跟随者 3.Observer:观察者 二.选主流程 三.数据一致性 四.脑裂及处理措施 1.为什么过半机 ...

  3. 浅谈Python http库 httplib2

    为什么80%的码农都做不了架构师?>>>    浅谈Python http库 httplib2 http.client 是实现了rfc 2616, http 协议的底层库 urlli ...

  4. 浅谈:飞鸽传书 的TCP/IP原理

    浅谈:飞鸽传书 的TCP/IP原理,adj  兴趣浓厚的 飞鸽传书:见闻广博的,博识的he is hot in mathematics and chemistry  (他数学和化学极好.)复合词语ho ...

  5. Zookeeper 客户端之 Curator

    之前写的一个在 Linux 上安装部署 Zookeeper 的笔记,其他操作系统请自行谷歌教程吧. 本文案例工程已经同步到了 github,传送门. PS : 目前还没有看过Curator的具体源码, ...

  6. 浅谈HTTPS通信机制和Charles抓包原理-by:nixs

    转载请注明出处:https://blog.csdn.net/zwjemperor/article/details/80719427 主页:https://blog.csdn.net/zwjempero ...

  7. 浅谈通过CMOS放电破解BIOS密码的原理

    浅谈CMOS放电破解BIOS密码的原理 ROM和RAM BIOS CMOS 主板电池 CMOS放电清除BIOS密码 电脑开机密码是软件密码,比较容易破解:BIOS密码是硬件密码,如果忘了BIOS密码该 ...

  8. 浅谈js开源库jQuery

    文章目录 0 前言 1 jQuery 核心函数 2 jQuery 对象和 dom 对象 3 jQuery 选择器 4 DOM 的增删改 5 jQuery 的属性操作 6 jQuery 事件操作 7.封 ...

  9. python functools partial_浅谈python标准库--functools.partial

    一.简单介绍: functools模块用于高阶函数:作用于或返回其他函数的函数.一般而言,任何可调用对象都可以作为本模块用途的函数来处理. functools.partial返回的是一个可调用的par ...

  10. 【OMNeT+INET】浅谈OMNeT开源库INET(二)

    [前言] 更多讨论欢迎加入学习QQ群:157696125 本文试图以简明的方式,结合实际项目使用需求,从几个角度对开源库INET进行简要说明:希望可对各位研究生同学和开发人员有益参考:对于其中可能存在 ...

最新文章

  1. 超级计算数据中心设计要求(征求意见稿)
  2. Swift之深入解析如何进行多重条件排序
  3. Vue_注册登录(短信验证码登录)
  4. git add 失效
  5. 网络爬虫之httpclient的使用
  6. Bootstrap 分页导航的尺寸
  7. 网页设计与网站组建标准教程(2013-2015版)
  8. bin文件如何编辑_每日学习:Linux文件与目录管理常用命令解析
  9. Visio ——一款能让你画图“走捷径”的软件
  10. 为什么要发布海外新闻稿,海外稿件怎么写
  11. 曹鹏CSS视频教程 编程之邦
  12. c语言程序员表白的语录,程序员一句话表白情书
  13. 百位大整数的加法雏形——C语言
  14. python数据分析 - T检验与F检验:二组数据那个更好?(一)
  15. 《评人工智能如何走向新阶段》后记(再续17)
  16. cpu2017的526.blender编译出错原因
  17. 关于嵌入式的技术竞争力
  18. 六度分离 (Floyd)
  19. 超级坦克大战1990 - 坦克大战超难版
  20. Linux下编译leveldb报错,leveldb ubuntu 11.04下编译失败问题

热门文章

  1. CF1039E Summer Oenothera Exhibition
  2. element的loading的蒙版导致滚动条消失,页面抖动
  3. 添加子节点时报错:TypeError: oUl.appendChild is not a function
  4. php获取当前文件夹下所有图片大小,PHP获取文件夹大小函数用法实例
  5. mysql5.7审计功能开启_开启mysql的审计功能
  6. java写培根披萨和海鲜披萨_java子类继承父类实例-披萨的选择实现代码
  7. 小程序仿微视_争抢流量!腾讯微视也要开始搞短视频带货
  8. mysql异构迁移_如何利用数据异构实现多级缓存和数据迁移
  9. c语言计算10亿位圆周率,C语言:圆周率的计算
  10. MQTT教程(一):MQTT简介