【分布式锁的演化】终章!手撸ZK分布式锁!
前言
这应该是分布式锁演化的最后一个章节了,相信很多小伙伴们看完这个章节之后在应对高并发的情况下,如何保证线程安全心里肯定也会有谱了。在实际的项目中也可以参考一下老猫的github上的例子,当然代码没有经过特意的封装,需要小伙伴们自己再好好封装一下。那么接下来,就和大家分享一下基于zookeeper的分布式锁,由于此篇主要分享的是zk的分布式锁,所以对于zk本身的相关知识点,并不会涉及很多。和分布式锁实现有关的zk知识点会提及。
Zookeeper实现分布式锁
何为ZK?(为了打字简单,后续老猫均以ZK来代替zookeeper),相信很多接触到Dubbo框架的小伙伴可能听说过ZK,但是具体也没有详细地去学习ZK。那么又如何利用ZK来实现分布式锁呢?以下我们一个个来看。
什么是ZK?
对于没有接触过ZK的小伙伴,老猫给个非专业但是挺实用的解释,ZK是一个分布式协调服务,该服务由N多个节点构成,每个节点均可存储数据。
数据结构
在了解锁原理之前我们先来看一下ZK的数据结构,具体如下:
在 Zookeeper 中,每一个数据节点都是一个 ZNode,上图根目录下有两个节点,分别是:app1 和 app2,其中 app1 下面又有三个子节点。那么我们来看看 ZNode 数据结构到底是什么样子的呢。首先我们来了解 ZNode 的类型。
Zookeeper 节点类型可以分为三大类:持久性节点(Persistent)、瞬时性节点(Ephemeral)、顺序性节点(Sequential)。现实开发中在创建节点的时候通过组合可以生成以下四种节点类型:持久节点、持久顺序节点、瞬时节点、瞬时有序节点。
(1) 持久节点:节点被创建后会一直存在服务器,直到删除操作主动清除,这种节点也是最常见的类型。
(2) 持久顺序节点:有顺序的持久节点,节点特性和持久节点是一样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后面加上一个数字后缀,来表示其顺序。
(3) 瞬时节点:会被自动清理掉的节点,它的生命周期和客户端会话绑在一起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建子节点。
(4)瞬时有顺序节点:有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀。
那么此次我们的ZK分布式锁就是基于ZK的临时有序节点实现的,也就是上述的第四种节点。当然光凭借第四种临时有序节点是不够的,我们还需要用到ZK的另外一个比较重要的概念,那就是“ZK观察器”。
ZK观察器
ZK观察器可以监测到节点的变动,如果节点发生变更会通知到客户端。我们可以设置观察器的三个方法:getData(),getChildrean(),exists()。观察器有一个比较重要的特性就是只能监控一次,再监控需要重新设置。
原理流程
(1)利用ZK的瞬时有序节点的特性。
(2)多线程并发创建瞬时节点时,得到有序的序列。
(3)序号最小的线程获得锁。
(4)其他的线程则监听自己节点序号的前一个序号。
(5)前一个线程执行完成,删除自己序号的节点。
(6)下一个序号的线程得到通知,继续执行。
(7)依次类推
通过上述流程大家就会发现,其实在创建节点的时候,就已经确定了线程的执行顺序。大家看完这个流程可能有点模糊,咱们继续看下面的图解,老猫相信大家心里就会有一个更加清晰的认知。
【流程一】我们有四个线程,分别是线程A、线程B、线程C、线程D。此时线程并发运行,这样就会在我们的ZK中创建四个临时有序节点,按照先来后到的顺序分别是1、2、3、4。此时按照我们流程描述中的第三点描述由于线程A对应的序号最小,所以A优先获取锁。
【流程二】再依次看第二个流程,此时当A获取锁之后,线程B的监听器会去监听1节点的执行情况,线程C的监听器会去监听2节点的执行情况,线程D的监听器会去监听3节点的执行情况依次类推。
【流程三】当线程A执行完毕之后会删除相关的节点1,此时会被线程B监听到,于是线程B开始执行,有线程C监听等待着线程B节点的释放,依次类推,直到这四个线程都执行完毕。
通过以上的图解,老猫觉得很多小伙伴对ZK锁的实现原理应该已经知道了,当然对ZK还是比较陌生的小伙伴也可以专门抽时间去熟悉一下ZK。接下来就和老猫一起来看一下具体的代码又是如何实现的吧。
纯手撸ZK分布式锁代码
基于上述的流程,我们手撸一下核心的代码,首先我们搭建的zk服务器必须和项目中使用的pom依赖是同一版本,这样也才能够避免出问题,由于老猫使用的是zk的3.6.2版本,所以老猫引入的pom如下:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.2</version>
</dependency>
手写zk锁的逻辑主要也是根据上述原理实现,代码中有比较晦涩难懂的地方,老猫也写了详细的备注,还有不 明白的铁子可以给老猫留言:
/**
* @author kdaddy@163.com
* @date 2021/1/16 10:25
* @公众号 程序员老猫
*/
@Slf4j
@Service
public class ZKLockUtil implements AutoCloseable, Watcher {
private ZooKeeper zooKeeper;
private String zNode;
public ZKLockUtil() throws Exception {
this.zooKeeper = new ZooKeeper("localhost:2181",100000,this);
}
public boolean getLock(String businessCode){
try {
// 首先创建业务根节点,类比之前的redis锁的key以及mysql锁的businessCode
Stat stat = zooKeeper.exists("/"+businessCode,false);
if(stat == null){
//表示创建一个业务根目录,此节点为持久节点,另外的由于在本地搭建的zk没有设置密码,所以采用OPEN_ACL_UNSAFE模式
zooKeeper.create("/" +businessCode,businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
//创建该目录下的有序瞬时节点,假如我们的订单业务编号是"order",那么第一个有序瞬时节点应该是/order/order_0000001
zNode =zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
/**
* 按照之前原理的时候的逻辑,
* 我们会对所有的节点进行排序并且序号最小的那个节点优先获取锁,
* 其他节点处于监听状态
*/
//此处获取所有子节点,注:之前文章中提及的getData(),getChildrean(),exists()的第二个参数表示是否设置观察器,ture为设置,false表示不设置
List<String> childrenNodes = zooKeeper.getChildren("/"+businessCode,false);
//子节点排序
Collections.sort(childrenNodes);
//获取序号最小的子节点
String minNode = childrenNodes.get(0);
//如果创建的节点是最小序号的节点,那么就获得锁
if(zNode.endsWith(minNode)){
return true;
}
//否则监听前一个节点的情况
/**
* 到这里说明创建的zNode为第二个或者第三第四个等节点
* 此处比较晦涩用代入法去理解
* 如果zNode是第二个节点,那么监听的就是第一个最小节点,
* 如果zNode是第三个节点,那么此时上一个节点就是循环中的当前那个节点。
* 需要细品
*/
String lastNode = minNode;
for (String node : childrenNodes){
//如果瞬时节点为非第一个节点,那么监听前一个节点
if(zNode.endsWith(node)){
zooKeeper.exists("/"+businessCode+"/"+lastNode,true);
break;
}else {
lastNode = node;
}
}
//并发情况下wait方法让出锁,但是由于并发情景下,为了避免释放的时候错乱因此加上synchronized
synchronized (this){
wait();
}
//当被唤起的时候相当于轮到了,当前拿到了锁,所以return true
return true;
}catch (Exception e){
e.printStackTrace();
}
return false;
}
@Override
public void process(WatchedEvent watchedEvent) {
//如果监听到节点被删除,那么则会通知下一个线程
if(watchedEvent.getType() == Event.EventType.NodeDeleted){
synchronized (this){
notify();
}
}
}
@Override
public void close() throws Exception {
zooKeeper.delete(zNode,-1);
zooKeeper.close();
log.info("我已经释放了锁!");
}
}
具体service层的代码老猫也做了更改,由于只看锁,所以在此老猫将相关落订单的逻辑去除了,对于上述工具类,可以进行如下使用:
/**
* @author kdaddy@163.com
* @date 2021/1/16 10:25
* @公众号 程序员老猫
*/
@Service
@Slf4j
public class ZKLockService {
@Autowired
private ZKLockUtil zkLockUtil;
private String ORDER_KEY = "order_kd";
public Integer createOrder() throws Exception{
log.info("进入了方法");
try {
if (zkLockUtil.getLock(ORDER_KEY)) {
log.info("拿到了锁");
//此处为了手动演示并发,所以我们暂时在这里休眠
Thread.sleep(6000);
}
}catch (Exception e){
e.printStackTrace();
}finally {
zkLockUtil.close();
}
log.info("方法执行完毕");
return 1;
}
}
上述即为实现代码,相关的逻辑,老猫也在代码的备注中阐释。如果还有不清楚的小伙伴可以给老猫留言。当然想要完整测试代码也可以去老猫的github地址下载。地址:https://github.com/maoba/kd-distribute
curator客户端的使用
相信有很多还是会有很多小伙伴会说,上述的流程逻辑比较绕,太让人头疼了。那么福利来了,其实关于ZK锁的话还有可以用封装比较完善的客户端,那就是curator。这个客户端本身就已经实现了ZK的分布式锁,咱们开箱调用即可。如果有更多的小伙伴想要了解curator,也可以去官网去研究一番,具体的地址为:http://curator.apache.org/。当然老猫下面的代码也是根据官网的步骤写出来的。具体代码实现如下:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.3.0</version>
</dependency>
由于curator每次启动都要连接zk,所以老猫干脆将其放在springboot的启动中。其实上面手写的通过构造方法连接zk的方式也可以做一下改造。
/**
* @author ktdaddy
* @公众号 程序员老猫
*/
@SpringBootApplication
@MapperScan("com.kd.distribute.dao")
public class DistributeApplication {
public static void main(String[] args) {
SpringApplication.run(DistributeApplication.class, args);
}
//启动服务的时候连接zk,并且指定开始使用和结束使用的方法
@Bean(initMethod="start",destroyMethod = "close")
public CuratorFramework getCuratorFramework() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
return client;
}
}
具体锁的使用代码如下:
/**
* @author kdaddy@163.com
* @date 2021/1/16 22:49
* @公众号 程序员老猫
*/
@Service
@Slf4j
public class CuratorLockService {
private String ORDER_KEY = "order_kd";
@Autowired
private CuratorFramework client;
public Integer createOrder() throws Exception{
log.info("进入了方法");
InterProcessMutex lock = new InterProcessMutex(client, "/"+ORDER_KEY);
try {
if (lock.acquire(30, TimeUnit.SECONDS)) {
log.info("拿到了锁");
//此处为了手动演示并发,所以我们暂时在这里休眠6s
Thread.sleep(6000);
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
log.info("我释放了锁!!");
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
log.info("方法执行完毕");
return 1;
}
}
相当简单,当然有兴趣研究源码实现的小伙伴也可以查看一下InterProcessMutex的相关的源码。在此老猫不赘述。
分布式锁的对比
到此,我们将分布式系统的锁的解决方案都已经和大家分享过了,最终咱们来进行一个对比,具体如下:
看了上面这个比较之后,其实在我们的实际项目中,还是推荐现成的 curator 实现方式以及redisson实现方式,因为毕竟目前来说是相当成熟的方案,不推荐由我们自己的代码去实现。所以小伙伴们在选择的时候就不用纠结了。
写在最后
老猫花了将近半个月的时候整理和输出了单体锁演化到分布式锁的解决方案,熬了比较多的夜,如果能给大家带来收获,那是再好不过的了。当然看到这里也希望能得到你的点赞、关注和转发。你的支持,是老猫原创的最大动力,后面老猫会带给大家更多分布式系统的解决方案。也希望能得到你的持续关注。更多精彩欢迎大家关注公众号“程序员老猫”
【分布式锁的演化】终章!手撸ZK分布式锁!的更多相关文章
- 手撕redis分布式锁,隔壁张小帅都看懂了!
前言 上一篇老猫和小伙伴们分享了为什么要使用分布式锁以及分布式锁的实现思路原理,目前我们主要采用第三方的组件作为分布式锁的工具.上一篇运用了Mysql中的select ...for update实现了 ...
- 《Spring 手撸专栏》第 3 章:初显身手,运用设计模式,实现 Bean 的定义、注册、获取
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你是否能预见复杂内容的设计问题? 讲道理,无论产品功能是否复杂,都有很大一部分程序员 ...
- 手撸基于swoole 的分布式框架 实现分布式调用(20)讲
最近看的一个swoole的课程,前段时间被邀请的参与的这个课程 比较有特点跟一定的深度,swoole的实战教程一直也不多,结合swoole构建一个新型框架,最后讲解如何实现分布式RPC的调用. 内容听 ...
- Zookeeper——基本使用以及应用场景(手写实现分布式锁和rpc框架)
文章目录 Zookeeper的基本使用 Zookeeper单机部署 Zookeeper集群搭建 JavaAPI的使用 Zookeeper的应用场景 分布式锁的实现 独享锁 可重入锁 实现RPC框架 基 ...
- 【分布式锁的演化】“超卖场景”,MySQL分布式锁篇
前言 之前的文章中通过电商场景中秒杀的例子和大家分享了单体架构中锁的使用方式,但是现在很多应用系统都是相当庞大的,很多应用系统都是微服务的架构体系,那么在这种跨jvm的场景下,我们又该如何去解决并发. ...
- 《Mybatis 手撸专栏》第1章:开篇介绍,我要带你撸 Mybatis 啦!
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 1. 为甚,撸Mybatis 我就知道,你会忍不住对它下手! 21年带着粉丝伙伴撸了一遍 Sp ...
- 《Mybatis 手撸专栏》第7章:SQL执行器的定义和实现
作者:小傅哥 博客:https://bugstack.cn - <手写Mybatis系列> 一.前言 为什么,要读框架源码? 因为手里的业务工程代码太拉胯了!通常作为业务研发,所开发出来的 ...
- 2022年整理最详细的java面试题、掌握这一套八股文、面试基础不成问题[吐血整理、纯手撸]
这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 1.面向对象 2.JDK.JRE.JVM区别和联系 3.==和equals 4.final 5.String .Strin ...
- 史上最简单的 SpringCloud 教程 | 终章
https://blog.csdn.net/forezp/article/details/70148833转载请标明出处:http://blog.csdn.net/forezp/article/det ...
随机推荐
- spark中map和mapPartitions算子的区别
区别: 1.map是对rdd中每一个元素进行操作 2.mapPartitions是对rdd中每个partition的迭代器进行操作 mapPartitions优点: 1.若是普通map,比如一个par ...
- socket ThreadingTCPServer学习笔记
文件上传#服务端 while True: conn,address = sk.accept() conn.sendall(bytes('欢迎你小sb',encoding='utf-8')) str_s ...
- Pytest 学习(二十五)- allure 命令行参数【转】
先看看 allure 命令的帮助文档 cmd 敲 allure -h allure 命令的语法格式 allure [options] [command] [command options] optio ...
- 升级jenkins之后无法启动 报错Unable to read /var/lib/jenkins/config.xml
故障记录 点击jenkins升级后再点击回滚到之前版本,jenkins就起不来了. 欲哭无泪,报错如下 hudson.util.HudsonFailedToLoad: org.jvnet.hudson ...
- OpenShift添加应用健康检查功能
什么是健康检查? 对于部署成功的应用来说,通过访问接口.执行特定命令等方式判断应用是否存活.正常的方式称为健康检查. 在 OpenShift 或 Kubernetes 中,健康检查都有两个探针,分别是 ...
- 面试 16-01.MVVM
16-01.MVVM #前言 MVVM的常见问题: 如何理解MVVM 如何实现MVVM 是否解读过Vue的源码 题目: 说一下使用 jQuery 和使用框架的区别 说一下对 MVVM 的理解 vue ...
- Python中的”黑魔法“与”骚操作“
本文主要介绍Python的高级特性:列表推导式.迭代器和生成器,是面试中经常会被问到的特性.因为生成器实现了迭代器协议,可由列表推导式来生成,所有,这三个概念作为一章来介绍,是最便于大家理解的,现在看 ...
- ASP.NET Core WebAPI实现本地化多语言(单资源文件)
在Startup ConfigureServices 注册本地化所需要的服务AddLocalization和 Configure<RequestLocalizationOptions> p ...
- 为什么MySQL不推荐使用uuid作为主键?
前言 在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一,单机递增),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么 ...
- java中如何实现同一账号不能同时登录
经过两天的研究,下面给两个方法.不个是webwork版本的,一个是修改过后的网上的意见监听器版本的 (一) 首先先上自己的研究成果 1:首先在baseAction 中或者直接在action中写一个方法 ...