Zookeeper分布式锁代码实现[通俗易懂]

目录原生API操作ZKWatch机制分布式锁思路Zookeeper分布式锁的代码实现zkclientCurator原生API操作ZK什么叫原生API操作ZK呢?实际上,利用zookeeper.jar这样的就是基于原生的API方式操作ZK,因为这个原生API使用起来并不是让人很舒服,于是出现了zkclient这种方式,以至到后来基于Curator框架,让人使用ZK…

大家好,又见面了,我是你们的朋友全栈君。

目录

原生API操作ZK 

Watch机制

分布式锁思路

Zookeeper分布式锁的代码实现

zkclient

Curator


原生API操作ZK 

什么叫原生API操作ZK呢?实际上,利用zookeeper.jar这样的就是基于原生的API方式操作ZK,因为这个原生API使用起来并不是让人很舒服,于是出现了zkclient这种方式,以至到后来基于Curator框架,让人使用ZK更加方便。有一句话,Guava is to JAVA what Curator is to Zookeeper。

Zookeeper分布式锁代码实现[通俗易懂]

说明:

在初始化Zookeeper时,有多种构造方法可以选择,有3个参数是必备的:connectionString(多个ZK SERVER之间以,分隔),sessionTimeout(就是zoo.cfg中的tickTime),Watcher(事件处理通知器)

需要注意的是ZK的连接是异步的,因此我们需要CountDownLatch来帮助我们确保ZK初始化完成。

对于事件(WatchedEvent)而言,有状态以及类型。

Zookeeper分布式锁代码实现[通俗易懂]

下面,我们来看一看基于原生API方式的增删改查:

Zookeeper分布式锁代码实现[通俗易懂]

注意,节点有2大类型,持久化节点、临时节点。在此基础上,又可以分为持久化顺序节点(PERSISTENT_SEQUENTIAL)、临时顺序节点(EPHEMERAL_SEQUENTIAL)。

节点类型只支持byte[],也就是说我们是无法直接给一个对象给ZK,让ZK帮助我们完成序列化操作的!

Zookeeper分布式锁代码实现[通俗易懂]

这里需要注意的是,原生API对于ZK的操作其实是分为同步和异步2种方式的。

rc表示return code,就是返回码,0即为正常。

path是传入API的参数,ctx也是传入的参数。

注意在删除过程中,是需要版本检查的,所以我们一般提供-1跳过版本检查机制。

Watch机制

ZK有watch事件,是一次性触发的。当watch监控的数据发生变化,会通知设置了该监控的client,即watcher。Zookeeper的watch是有自己的一些特性的:

  1. 一次性:请牢记,just watch one time! 因为ZK的监控是一次性的,所以每次必须设置监控。
  2. 轻量:WatchedEvent是ZK进行watch通知的最小单元,整个数据结构包含:事件状态、事件类型、节点路径。注意ZK只是通知client节点的数据发生了变化,而不会直接提供具体的数据内容。
  3. 客户端串行执行机制:注意客户端watch回调的过程是一个串行同步的过程,这为我们保证了顺序,我们也应该意识到不能因一个watch的回调处理逻辑而影响了整个客户端的watch回调。

下面我们来直接看代码:

Zookeeper分布式锁代码实现[通俗易懂]

Zookeeper分布式锁代码实现[通俗易懂]

Zookeeper分布式锁代码实现[通俗易懂]

一定得注意的是,监控该节点和监控该节点的子节点是2码子事。

比如exists(path,true)监控的就是该path节点的create/delete/setData;getChildren(path,watcher)监控的就是该path节点下的子节点的变化(子节点的创建、修改、删除都会监控到,而且事件类型都是一样的,想一想如何区分呢?给一个我的思路,就是我们得先有该path下的子节点的列表,然后watch触发后,我们对比下该path下面的子节点SIZE大小及内容,就知道是增加的是哪个子节点,删除的是哪个子节点了!)

getChildren(path,true)和getChildren(path,watcher)有什么区别?前者是沿用上下文中的Watcher,而后者则是可以设置一个新的Watcher的!(因此,要想做到一直监控,那么就有2种方式,一个是注意每次设置成true,或者干脆每次设置一个新的Watcher)

从上面的讨论中,你大概能了解到原生的API其实功能上还不是很强大,有些还得我们去操心,到后面为大家介绍Curator框架,会有更好的方式进行处理。

分布式锁思路

首先,我们不谈Zookeeper是如何帮助我们处理分布式锁的,而是先来想一想,什么是分布式锁?为什么需要分布式锁?有哪些场景呢?分布式锁的使用又有哪些注意的?分布式锁有什么特性呢?

说起锁,我们自然想到Java为我们提供的synchronized/Lock,但是这显然不够,因为这只能针对一个JVM中的多个线程对共享资源的操作。那么对于多台机器,多个进程对同一类资源进行操作的话,就是所谓分布式场景下的锁。

各个电商平台经常搞的“秒杀”活动需要对商品的库存进行保护、12306火车票也不能多卖,更不允许一张票被多个人买到、这样的场景就需要分布式锁对共享资源进行保护!

既然,Java在分布式场景下的锁已经无能为力,那么我们只能借助其他东西了!

我们的老朋友:DB

对,没错,我们能否借助DB来实现呢?要知道DB是有一些特点供我们利用的,比如DB本身就存在锁机制(表锁、行锁),唯一约束等等。

假设,我们的DB中有一张表T(id,methodname,ip,threadname,……),其中id为主键,methodname为唯一索引。

对于多台机器,每台机器上的多个线程而言,对一个方法method进行操作前,先select下T表中是否存在method这条记录,如果没有,就插入一条记录到T中。当然可能并发select,但是由于T表的唯一约束,使得只有一个请求能插入成功,即获得锁。至于释放锁,就是方法执行完毕后delete这条记录即可。

考虑一些问题:如果DB挂了,怎么办?如果由于一些因素,导致delete没有执行成功,那么这条记录会导致该方法再也不能被访问!为什么要先select,为什么不直接insert呢?性能如何呢?

为了避免单点,可以主备之间实现切换;为了避免死锁的产生,那么我们可以有一个定时任务,定期清理T表中的记录;先select后insert,其实是为了保证锁的可重入性,也就是说,如果一台IP上的某个线程获取了锁,那么它可以不用在释放锁的前提下,继续获得锁;性能上,如果大量的请求,将会对DB考验,这将成为瓶颈。

到这里,还有一个明显的问题,需要我们考虑:上述的方案,虽然保证了只会有一个请求获得锁,但其他请求都获取锁失败返回了,而没有进行锁等待!当然,我们可以通过重试机制,来实现阻塞锁,不过数据库本身的锁机制可以帮助我们完成。别忘了select … for update这种阻塞式的行锁机制,commit进行锁的释放。而且对于for update这种独占锁,如果长时间不提交释放,会一直占用DB连接,连接爆了,就跪了!

不说了,老朋友也只能帮我们到这里了!

我们的新朋友:Redis or 其他分布式缓存(Tair/…)

既然说是缓存,相较DB,有更好的性能;既然说是分布式,当然避免了单点问题;

比如,用Redis作为分布式锁的setnx,这里我就不细说了,总之分布式缓存需要特别注意的是缓存的失效时间。(有效时间过短,搞不好业务还没有执行完毕,就释放锁了;有效时间过长,其他线程白白等待,浪费了时间,拖慢了系统处理速度)

看Zookeeper是如何帮助我们实现分布式锁

Zookeeper中临时顺序节点的特性:

第一,节点的生命周期和client回话绑定,即创建节点的客户端回话一旦失效,那么这个节点就会被删除。(临时性)

第二,每个父节点都会维护子节点创建的先后顺序,自动为子节点分配一个×××数值,以后缀的形式自动追加到节点名称中,作为这个节点最终的节点名称。(顺序性)

那么,基于临时顺序节点的特性,Zookeeper实现分布式锁的一般思路如下:

1.client调用create()方法创建“/root/lock_”节点,注意节点类型是EPHEMERAL_SEQUENTIAL

2.client调用getChildren(“/root/lock_”,watch)来获取所有已经创建的子节点,并同时在这个节点上注册子节点变更通知的Watcher

3.客户端获取到所有子节点Path后,如果发现自己在步骤1中创建的节点是所有节点中最小的,那么就认为这个客户端获得了锁

4.如果在步骤3中,发现不是最小的,那么等待,直到下次子节点变更通知的时候,在进行子节点的获取,判断是否获取到锁

5.释放锁也比较容易,就是删除自己创建的那个节点即可

上面的这种思路,在集群规模很大的情况下,会出现“羊群效应”(Herd Effect):

在上面的分布式锁的竞争中,有一个细节,就是在getChildren上注册了子节点变更通知Watcher,这有什么问题么?这其实会导致客户端大量重复的运行,而且绝大多数的运行结果都是判断自己并非是序号最小的节点,从而继续等待下一次通知,也就是很多客户端做了很多无用功。更加要命的是,在集群规模很大的情况下,这显然会对Server的性能造成影响,而且一旦同一个时间,多个客户端断开连接,服务器会向其余客户端发送大量的事件通知,这就是所谓的羊群效应!

出现这个问题的根源,其实在于,上述的思路并没有找准客户端的“痛点”:

客户端的核心诉求在于判断自己是否是最小的节点,所以说每个节点的创建者其实不用关心所有的节点变更,它真正关心的应该是比自己序号小的那个节点是否存在!

1.client调用create()方法创建“/root/lock_”节点,注意节点类型是EPHEMERAL_SEQUENTIAL

2.client调用getChildren(“/root/lock_”,false)来获取所有已经创建的子节点,这里并不注册任何Watcher

3.客户端获取到所有子节点Path后,如果发现自己在步骤1中创建的节点是所有节点中最小的,那么就认为这个客户端获得了锁

4.如果在步骤3中,发现不是最小的,那么找到比自己小的那个节点,然后对其调用exist()方法注册事件监听

5.之后一旦这个被关注的节点移除,客户端会收到相应的通知,这个时候客户端需要再次调用getChildren(“/root/lock_”,false)来确保自己是最小的节点,然后进入步骤3

Zookeeper分布式锁的代码实现

在上面的文章中,从思路上已经分析了Zookeeper如何帮助我们实现分布式锁,我们直接来看代码:

[分布式客户端]

Zookeeper分布式锁代码实现[通俗易懂]

[获取分布式锁的方法lock:初始化ZK]

Zookeeper分布式锁代码实现[通俗易懂]

[获取分布式锁的方法lock:创建临时节点与判断最小路径]

Zookeeper分布式锁代码实现[通俗易懂]

[main测试]

Zookeeper分布式锁代码实现[通俗易懂]

[运行结果]

Zookeeper分布式锁代码实现[通俗易懂]

需要注意的是,即便监控到了比自己序号小的节点的删除Watcher,也需要再次确认下!

从结果上,看的很清楚,各个线程有序获得锁。

zkclient

zkclient是在zookeeper原生API基础上做了一点封装,简化了ZK的复杂性。

来看代码:

Zookeeper分布式锁代码实现[通俗易懂]

我们观察下zkclient的使用,和以前基于zookeeper的原生API有哪些区别呢?

  • 第一,原生API需要我们利用CountDownLatch来确保ZK的初始化,现在zkclient帮助我们屏蔽掉了这个细节
  • 第二,原生API是不可以递归创建节点的,而zkclient可以帮助我们递归创建不存在的父节点,还可以递归删除
  • 第三,支持序列化操作,上面的代码你大概可以看出一些端倪,就是我们从操作byte[]到操作String了。(事实上,在zkclient中你只需要实现ZkSerializer接口,就可以完成Object到byte[]的转换,虽然如此,但是实际开发中,利用JSON也挺好的!)
  • 第四,还有最重要的一点就是,zkclient将对节点的操作和对节点的监控分离开了,在原生API中2者是耦合在一起的!从思想上来看,便于理解;从代码上来看,也简洁些(如果写在一起,头都大了);更加方便的是,zkclient替我们完成了重复watch的功能!

[watch订阅机制]

Zookeeper分布式锁代码实现[通俗易懂]

看到没有,是不是有点像MQ的订阅机制,非常好用!【但是也有点不太完美,子节点的数据变更为什么没有监控呢,这有点不符合人性啊!还好有Curator…】

但是呢,我们知道ZK是有很多应用场景的,比如实现分布式锁,zkclient并没有替我们进行封装,但是Curator框架可以帮助我们做到!

Curator

为了更好实现Java操作Zookeeper服务器,后来出现Curator框架,功能非常强大,目前已经是Apache的顶级项目,有很多丰富的特性,比如session超时重连,主从选举,分布式计数器,分布式锁等,非常有利于Zookeeper复杂场景下的开发。

POM文件:

Zookeeper分布式锁代码实现[通俗易懂]

增删改查:

Zookeeper分布式锁代码实现[通俗易懂]

Curator框架使用链式编程风格,易读性很强!

注意,不论是原生的API,还是基于zkclient的API,都是提供的connectTimeout,而Curator提供了sessionTimeout,功能很强大。

Zookeeper分布式锁代码实现[通俗易懂]

无论是原生的API,还是zkclient,都是支持异步回调的,但是Curator框架在支持异步回调的同时,增加了线程池供我们优化!

[NodeCacheListener]

Zookeeper分布式锁代码实现[通俗易懂]

[PathChildrenCacheListener]

Zookeeper分布式锁代码实现[通俗易懂]

对于Curator而言,为了解决重复Watch的问题,它引入了一种全新的思想:Cache与ZK SERVER比对的机制。不论是原生的API,还是基于ZKCLIENT的,其实它们解决思路都是重复注册!

思路决定出路!Curator通过事件驱动将客户端的Cache与ZK SERVER的数据比对,就自然而然的解决了重复WATCH的功能!为什么Curator能成为Apache的顶级项目呢,我想大概就是因为它的与众不同的设计思想!

在Curator中,有2种Listener,一个是监控节点的NodeCacheListener,一个是监控子节点的PathChildrenCacheListener。PathChildernCacheListener可以监控子节点的新增、修改、删除,非常好用!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/128705.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • IO编程

    IO编程是什么?    IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。    比如你打开浏览器,访问新浪首页,浏览器这个程序就需要通过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器,告诉它我想要首页的HTML,这个动作是往外发数据,叫O

  • BigDecimal的除法

    BigDecimal的除法BigDecimaldivideBigDecimal=subBigDecimal.divide(newBigDecimal(13),0,BigDecimal.ROUND_HALF_UP);第一参数表示除数,第二个参数表示小数点后保留位数,第三个参数表示舍入模式,只有在作除法运算或四舍五入时才用到舍入模式,有下面这几种ROUND_CEILING//向正无穷方向…

  • python spark教程菜鸟教程_菜鸟教程hadoop-hadoop入门教程「建议收藏」

    python spark教程菜鸟教程_菜鸟教程hadoop-hadoop入门教程「建议收藏」菜鸟教程hadoop-hadoop入门教程菜鸟教程hadoop-本站旨在为从事大数据行业或学习hadoop技术人员提供一个交流学习平台,全面收集了在大数据时代的互联网巨头企业的hadoop入门教程精华技术文章Lastly,wevariedthenumberofnodesbetween10and200withstepsize10.Meanwhile,thenumb…

  • StackOverflow 提问艺术[通俗易懂]

    StackOverflow 提问艺术[通俗易懂]在黑客的世界里,当你拋出一个技术问题时,最终是否能得到有用的回答,往往取决于你所提问和追问的方式。本指南将教你如何正确的提问以获得你满意的答案。https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md转载于:https://www.cnblogs.com/s…

  • 十七岁的单车 影评

    十七岁的单车 影评

  • response.sendRedirect()与request.getRequestDispatcher().forward()区别

    response.sendRedirect()与request.getRequestDispatcher().forward()区别Servlet中response.sendRedirect()与request.getRequestDispatcher().forward(request,response)这两个对象都可以使页面跳

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号