面试必备:基于 Zookeeper 的分布式锁实现【图文并茂 附源码 】

最近在学习 Zookeeper,在刚开始接触 Zookeeper 的时候,完全不知道 Zookeeper 有什么用。且很多资料都是将 Zookeeper 描述成一个“类 Unix/Linux 文件系统”的中间件,导致我很难将类 Unix/Linux 文件系统的 Zookeeper 和分布式应用联系在一起。后来在粗读了《ZooKeeper 分布式过程协同技术详解》和《从Paxos到Zookeeper 分布式一致性原理与实践》两本书,并动手写了一些 CURD demo 后,初步对 Zookeeper 有了一定的了解。不过比较肤浅,为了进一步加深对 Zookeeper 的认识,我利用空闲时间编写了本篇文章对应的 demo – 基于 Zookeeper 的分布式锁实现。通过编写这个分布式锁 demo,使我对 Zookeeper 的 watcher 机制、Zookeeper 的用途等有了更进一步的认识。不过我所编写的分布式锁还是比较简陋的,实现的也不够优美,仅仅是个练习,仅供参考使用。好了,题外话就说到这里,接下来我们就来聊聊基于 Zookeeper 的分布式锁实现。

2. 独占锁和读写锁的实现

在本章,我将分别说明独占锁和读写锁详细的实现过程,并配以相应的流程图帮助大家了解实现的过程。这里先说说独占锁的实现。

2.1 独占锁的实现

独占锁又称排它锁,从字面意思上很容易理解他们的用途。即如果某个操作 O1 对访问资源 R1 的过程加锁,在操作 O1 结束对资源 R1 访问前,其他操作不允许访问资源 R1。以上算是对独占锁的简单定义了,那么这段定义在 Zookeeper 的“类 Unix/Linux 文件系统”的结构中是怎样实现的呢?在锁答案前,我们先看张图:

图1 独占锁的 Zookeeper 节点结构

如上图,对于独占锁,我们可以将资源 R1 看做是 lock 节点,操作 O1 访问资源 R1 看做创建 lock 节点,释放资源 R1 看做删除 lock 节点。这样我们就将独占锁的定义对应于具体的 Zookeeper 节点结构,通过创建 lock 节点获取锁,删除节点释放锁。详细的过程如下:

1、多个客户端竞争创建 lock 临时节点 2、其中某个客户端成功创建 lock 节点,其他客户端对 lock 节点设置 watcher 3、持有锁的客户端删除 lock 节点或该客户端崩溃,由 Zookeeper 删除 lock 节点 4、其他客户端获得 lock 节点被删除的通知 5、重复上述4个步骤,直至无客户端在等待获取锁了

上面即独占锁具体的实现步骤,理解起来并不复杂,这里不再赘述。

图2 获取独占锁流程图

2.2 读写锁的实现

说完独占锁的实现,这节来说说读写锁的实现。读写锁包含一个读锁和写锁,操作 O1 对资源 R1 加读锁,且获得了锁,其他操作可同时对资源 R1 设置读锁,进行共享读操作。如果操作 O1 对资源 R1 加写锁,且获得了锁,其他操作再对资源 R1 设置不同类型的锁都会被阻塞。总结来说,读锁具有共享性,而写锁具有排他性。那么在 Zookeeper 中,我们可以用怎样的节点结构实现上面的操作呢?

图3 读写锁的 Zookeeper 节点结构

在 Zookeeper 中,由于读写锁和独占锁的节点结构不同,读写锁的客户端不用再去竞争创建 lock 节点。所以在一开始,所有的客户端都会创建自己的锁节点。如果不出意外,所有的锁节点都能被创建成功,此时锁节点结构如图3所示。之后,客户端从 Zookeeper 端获取 /share_lock 下所有的子节点,并判断自己能否获取锁。如果客户端创建的是读锁节点,获取锁的条件(满足其中一个即可)如下:

1、自己创建的节点序号排在所有其他子节点前面 2、自己创建的节点前面无写锁节点

如果客户端创建的是写锁节点,由于写锁具有排他性。所以获取锁的条件要简单一些,只需确定自己创建的锁节点是否排在其他子节点前面即可。

不同于独占锁,读写锁的实现稍微复杂一下。读写锁有两种实现方式,各有异同,接下来就来说说这两种实现方式。

读写锁的第一种实现

第一种实现是对 /sharelock 节点设置 watcher,当 /sharelock 下的子节点被删除时,未获取锁的客户端收到 /share_lock 子节点变动的通知。在收到通知后,客户端重新判断自己创建的子节点是否可以获取锁,如果失败,再次等待通知。详细流程如下:

1、所有客户端创建自己的锁节点 2、从 Zookeeper 端获取 /sharelock 下所有的子节点,并对 /sharelock 节点设置 watcher 3、判断自己创建的锁节点是否可以获取锁,如果可以,持有锁。否则继续等待 4、持有锁的客户端删除自己的锁节点,其他客户端收到 /share_lock 子节点变动的通知 5、重复步骤2、3、4,直至无客户端在等待获取锁了

上述步骤对于的流程图如下:

图4 获取读写锁实现1流程图

上面获取读写锁流程并不复杂,但却存在性能问题。以图3所示锁节点结构为例,第一个锁节点 host1-W-0000000001 被移除后,Zookeeper 会将 /share_lock 子节点变动的通知分发给所有的客户端。但实际上,该子节点变动通知除了能影响 host2-R-0000000002 节点对应的客户端外,分发给其他客户端则是在做无用功,因为其他客户端即使获取了通知也无法获取锁。所以这里需要做一些优化,优化措施是让客户端只在自己关心的节点被删除时,再去获取锁。

读写锁的第二种实现

在了解读写锁第一种实现的弊端后,我们针对这一实现进行优化。这里客户端不再对 /share_lock 节点进行监视,而只对自己关心的节点进行监视。还是以图3的锁节点结构进行举例说明,host2-R-0000000002 对应的客户端 C2 只需监视 host1-W-0000000001 节点是否被删除即可。而 host3-W-0000000003 对应的客户端 C3 只需监视 host2-R-0000000002 节点是否被删除即可,只有 host2-R-0000000002 节点被删除,客户端 C3 才能获取锁。而 host1-W-0000000001 节点被删除时,产生的通知对于客户端 C3 来说是无用的,即使客户端 C3 响应了通知也没法获取锁。这里总结一下,不同客户端关心的锁节点是不同的。如果客户端创建的是读锁节点,那么客户端只需找出比读锁节点序号小的最后一个的写锁节点,并设置 watcher 即可。而如果是写锁节点,则更简单,客户端仅需对该节点的上一个节点设置 watcher 即可。详细的流程如下:

1、所有客户端创建自己的锁节点 2、从 Zookeeper 端获取 /share_lock 下所有的子节点 3、判断自己创建的锁节点是否可以获取锁,如果可以,持有锁。否则对自己关心的锁节点设置 watcher 4、持有锁的客户端删除自己的锁节点,某个客户端收到该节点被删除的通知,并获取锁 5、重复步骤4,直至无客户端在等待获取锁了

上述步骤对于的流程图如下:

图5 获取读写锁实现2流程图

3. 写在最后

本文较为详细的描述了基于 Zookeeper 分布式锁的实现过程,并根据上面描述的两种锁原理实现了较为简单的分布式锁 demo,代码放在了 github 上,需要的朋友自取。因为这只是一个简单的 demo,代码实现的并不优美,仅供参考。最后,如果你觉得文章还不错的话,欢迎点赞。如果有不妥的地方,也请提出来,我会虚心改之。

参考

《ZooKeeper 分布式过程协同技术详解》 

《从Paxos到Zookeeper 分布式一致性原理与实践》

原文链接: 

https://www.tianxiaobo.com/2018/01/20/基于-Zookeeper-的分布式锁实现/

GitHub 代码:

https://github.com/code4wt/distributed_lock

往期精彩文章

Mybatis合集:

Mybatis源码分析之SqlSessionFactory(一)

Mybatis源码分析之SqlSession和Excutor(二)

Mybatis源码分析之Mapper执行SQL过程(三)

Mybatis源码分析之Cache一级缓存原理(四)

Mybatis源码分析之Cache二级缓存原理 (五)

mybatis结合redis实战二级缓存(六)

Spring源码分析:

【Spring源码】Spring IOC 容器源码分析(一)

【Spring源码】Spring IOC 容器源码分析(二)

【Spring源码】Spring IOC 容器源码分析(三)

Spring AOP源码分析:

Spring AOP 使用介绍,从前世到今生

Spring AOP 源码解析

NIO源码分析:

Java NIO:Buffer、Channel 和 Selector

Java 非阻塞 IO 和异步 IO

Tomcat 中的 NIO 源码分析

Netty源码分析

认真的 Netty 源码解析(一)

认真的 Netty 源码解析(二)

并发编程:

Java 读写锁 ReentrantReadWriteLock 源码分析

深度解读Java 线程池设计思想及源码实现

Java分布式中间件大汇聚实战第一季 (SpringBoot2.0+应用案例+点赞系统+面试突击)

03-19
概要介绍: 本门课程属于“Java分布式中间件大汇聚实战”系列课程,主要介绍了企业级项目中真实的应用场景的实现及主流的Java核心技术栈(Redis、RabbitMQ、Spring AOP、Redisson、ZooKeeper…)的实战等等。除此之外,还介绍了如何基于Redis设计并实战一款点赞系统(点赞、取消点赞、排行榜、用户中心、文章点赞用户列表…)可以说技术干货甚多,不仅可以巩固企业级应用系统的开发实战能力,相信在面试、跳槽涨薪方面也能带来相应的帮助! 课程内容: 传说中的金三银四、面试跳槽涨薪季已经来临,Debug特地为大家准备了一系列跟面试、跳槽、巩固核心技术栈相关的课程,本门课程属于第一季,其中的内容包括企业级项目中真实的应用场景实战、面试相关的技术点分享、主流的Java技术栈(Undertow、Redis、RabbitMQ、Spring AOP、Redisson、ZooKeeper…)实战等等。 除此之外,我们还基于Redis设计并实战了一款点赞系统,可以说技术干货甚多。在课程的最后,Debug给大家整理了一份最新的面向BAT大厂招聘 ~ 2020年程序猿最新的Java面试题(带目录和答案),希望对各位小伙伴的成长有所帮助! 值得一提的是,本季课程实战的应用场景包括“日志记录”、“邮件发送”、“通告消息通知”、“短信验证码失效验证”、“会员到期自动提醒/到期前N天自动提醒”以及“点赞系统”的设计与实战,其大纲如下所示: 其中,涉及到的技术栈包括Spring Boot2.0、Mybatis、Undertow、Redis、RabbitMQ、Redisson、Spring AOP、 Java8…下面罗列出本门课程重点介绍的价格应用案例以及业务场景的实现流程图! (1)基于Spring的消息驱动模型实现日志的异步记录: (2)基于消息中间件RabbitMQ的消息队列实现日志的异步记录: (3)基于缓存中间件Redis的订阅发布机制实现商户公告消息通知: (4)基于Redis的Key失效与定时任务实现实现短信验证码的过期失效验证: 其他核心、典型的应用案例和业务场景的实战可以详细参考“课程目录”! 除此之外,我们还基于缓存中间件Redis设计并实战实现了点赞系统中的点赞功能模块,下面罗列出其中涉及到的相关功能模块的实战流程图: 其课程收益如下所示:
©️2020 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值