一、简介

  在日常开发过程中,大型的项目一般都会采用分布式架构,那么在分布式架构中若需要同时对一个变量进行操作时,可以采用分布式锁来解决变量访问冲突的问题,最典型的案例就是防止库存超卖,当然还有其他很多的控制方式,这篇文章我们讨论一下怎么使用ZooKeeper来实现分布式锁。

二、Curator

  前面提到的分布式锁,在ZooKeeper中可以通过Curator来实现。

  定义:Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等等。

三、尝试写一个分布式锁

开发思路

  在第一篇文章中,了解了ZooKeeper节点的概念,实现分布式锁的基本思路也是基于对节点的监听与操作从而实现的。

  • 1、创建一个父节点,并对父节点设置监听事件,实际加锁的对象为父节点下的子节点。
  • 2、若父节点下存在临时子节点,则获取锁失败,不存在子节点时,则各个线程可尝试争夺锁。
  • 3、业务逻辑执行完毕后会删除临时子节点,此时下一个进程进入时发现没有存在子节点,则创建子节点并获取锁

四、写一个工具类

pom.xml

<!--Zookeeper实现分布式锁的工具curator start -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.8.0</version>
</dependency>
<!--Zookeeper实现分布式锁的工具curator end -->

application.yml

#zookeeper分布式锁curator服务配置
curator:
retryCount: 5 #重试次数
elapsedTimeMs: 5000 #重试间隔时间
connectString: 127.0.0.1:2181 # zookeeper 地址
sessionTimeoutMs: 60000 # session超时时间
connectionTimeoutMs: 5000 # 连接超时时间

配置类

/**
* ZK的属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "curator")//获取application.yml配置的值
public class ZkProperties { private int retryCount;//重试次数 private int elapsedTimeMs;//重试间隔时间 private String connectString;//zookeeper 地址 private int sessionTimeoutMs;//session超时时间 private int connectionTimeoutMs;//连接超时时间
}
/**
* ZK的属性配置
*/
@Configuration//标识这是一个配置类
public class ZkConfiguration { @Autowired
ZkProperties zkProperties; @Bean(initMethod = "start")
public CuratorFramework curatorFramework() {
return CuratorFrameworkFactory.newClient(
zkProperties.getConnectString(),
zkProperties.getSessionTimeoutMs(),
zkProperties.getConnectionTimeoutMs(),
new RetryNTimes(zkProperties.getRetryCount(), zkProperties.getElapsedTimeMs()));
}
}

分布式锁工具

/**
* 分布式锁工具类
* 【类解析】
* 1、InitializingBean接口为bean提供了初始化方法的方式。
* 2、它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。
* 【锁原理】
* 1、创建一个父节点,并对父节点设置监听事件,实际加锁的对象为父节点下的子节点。
* 2、若父节点下存在临时子节点,则获取锁失败。不存在子节点时,则各个线程可尝试争夺锁。
* 3、业务逻辑执行完毕后会删除临时子节点,此时下一个进程进入时发现没有存在子节点,则创建子节点并获取锁
*/
@Slf4j
@Service
public class DistributedLockByZookeeperUtil implements InitializingBean { private final static String ROOT_PATH_LOCK = "rootlock";//父节点路径
private CountDownLatch countDownLatch = new CountDownLatch(1);//节点计数器 @Autowired
private CuratorFramework curatorFramework; /**
* 获取分布式锁
*/
public void acquireDistributedLock(String path) {
String keyPath = "/" + ROOT_PATH_LOCK + "/" + path;
//1、一直循环等待获取锁
while (true) {
try {
//2、尝试创建子节点,若子节点已经存在,则创建异常,并进入catch块代码
curatorFramework
.create()//创建节点
.creatingParentsIfNeeded()//如果父节点不存在,则在创建节点的同时创建父节点
.withMode(CreateMode.EPHEMERAL)//【临时节点】创建后,会话结束节点会自动删除
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)//【接入权限】任何链接都可以操作该节点
.forPath(keyPath);//对应的操作路径
log.info("获取分布式锁成功!路径为:{}", keyPath);
break;
} catch (Exception e) {
//3、创建子节点失败时,即获取锁失败
log.info("获取分布式锁失败!路径为:{}", keyPath);
log.info("等待重新获取锁.......");
try {
if (countDownLatch.getCount() <= 0) {
countDownLatch = new CountDownLatch(1);//重置计数器
}
//4、从新挂起当前线程,调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
countDownLatch.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
} /**
* 释放分布式锁
*/
public boolean releaseDistributedLock(String path) {
try {
String keyPath = "/" + ROOT_PATH_LOCK + "/" + path;
//1、查看当前节点是否已经存在
if (curatorFramework.checkExists().forPath(keyPath) != null) {
//2、若子节点存在,则删除子节点,即释放锁
curatorFramework.delete().forPath(keyPath);
}
} catch (Exception e) {
log.error("释放分布式锁错误!");
return false;
}
return true;
} /**
* 创建 watcher 事件
*/
private void addWatcher(String path) throws Exception {
String keyPath;
if (path.equals(ROOT_PATH_LOCK)) {
keyPath = "/" + path;
} else {
keyPath = "/" + ROOT_PATH_LOCK + "/" + path;
}
//1、创建子节点监听事件
final PathChildrenCache cache = new PathChildrenCache(curatorFramework, keyPath, false);
//2、设置监听器初始化模式:异步初始化。初始化后会触发事件。
cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
//3、创建监听事件
cache.getListenable().addListener((client, event) -> {
//4、当发生子节点移除事件时,进入if内逻辑
if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
String oldPath = event.getData().getPath();
log.info("上一个节点 " + oldPath + " 已经被断开");
//5、移除的节点为监听节点的子节点时,即路径包含父节点时,进入if内逻辑
if (oldPath.contains(path)) {
//6、释放计数器,让当前的请求获取锁
countDownLatch.countDown();
}
}
});
} /**
* 创建父节点,并创建永久节点
* PS:在所有的属性被初始化后调用此方法,创建父节点
*/
@Override
public void afterPropertiesSet() {
//1、指定命名空间
curatorFramework = curatorFramework.usingNamespace("lock-namespace");
//2、下面代码逻辑的父节点路径
String path = "/" + ROOT_PATH_LOCK;
try {
//3、父节点不存在时,创建父节点
if (curatorFramework.checkExists().forPath(path) == null) {
curatorFramework
.create()//创建节点
.creatingParentsIfNeeded()//如果父节点不存在,则在创建节点的同时创建父节点
.withMode(CreateMode.PERSISTENT)//【持久化节点】客户端与zookeeper断开连接后,该节点依旧存在
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)//【接入权限】任何链接都可以操作该节点
.forPath(path);//对应的操作路径
}
//4、添加对父节点的监听事件
addWatcher(ROOT_PATH_LOCK);
log.info("root path 的 watcher 事件创建成功");
} catch (Exception e) {
log.error("连接zookeeper失败,请查看日志 >> {}", e.getMessage(), e);
}
}
}

测试类

/**
* 测试类
*/
@RestController
@RequestMapping("/test")
public class TestController { @Autowired
private DistributedLockByZookeeperUtil distributedLockByZookeeper;//分布式锁工具类
@Autowired
private IUserInfoService iUserInfoService;//业务类 private final static String PATH = "test";//子节点对应路径(PS:在锁工具里面会拼接完整路径) @GetMapping("/doSomeThings")
public boolean doSomeThings() {
/*1、获取锁*/
Boolean flag;//是否已经释放锁 释放成功:true , 释放失败:false
distributedLockByZookeeper.acquireDistributedLock(PATH);//获取锁
/*2、业务代码块*/
try {
iUserInfoService.update();
UserInfoVO vo = iUserInfoService.querySingleVO(1);
System.out.println("剩余库存为:" + vo.getCreateStaff());
} catch (Exception e) {
e.printStackTrace();
//业务代码报错时及时释放锁
flag = distributedLockByZookeeper.releaseDistributedLock(PATH);
}
/*3、释放锁*/
flag = distributedLockByZookeeper.releaseDistributedLock(PATH);//执行成功释放锁
return flag;
} }

五、压测方法

  压测的方法有很多,我使用的是Jmeter来进行并发调用测试类代码,测试结果分布式锁有效,这里不再写压测过程,感兴趣的亲可以看下文末的文章推荐。

参考文章:

ZooKeeper学习(二)ZooKeeper实现分布式锁的更多相关文章

  1. zookeeper学习实践1-实现分布式锁

    引言 ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件.它是一个为分布式应用提供一致性服务的软件,提 ...

  2. ZooKeeper学习笔记四:使用ZooKeeper实现一个简单的分布式锁

    作者:Grey 原文地址: ZooKeeper学习笔记四:使用ZooKeeper实现一个简单的分布式锁 前置知识 完成ZooKeeper集群搭建以及熟悉ZooKeeperAPI基本使用 需求 当多个进 ...

  3. ZooKeeper学习之-Zookeeper简单介绍(一)

    一.分布式协调技术 在给大家介绍ZooKeeper之前先来给大家介绍一种技术——分布式协调技术.那么什么是分布式协调技术?那么我来告诉大家,其实分布式协调技术主要用来解决分布式环境当中多个进程之间的同 ...

  4. 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

  5. Zookeeper--0300--java操作Zookeeper,临时节点实现分布式锁原理

    删除Zookeeper的java客户端有  : 1,Zookeeper官方提供的原生API, 2,zkClient,在原生api上进行扩展的开源java客户端 3, 一.Zookeeper原生API ...

  6. ZooKeeper典型应用场景:分布式锁

    分布式锁是控制分布式系统之间同步访问共享资源的一种方式.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致 ...

  7. Docker 下的Zookeeper以及.ne core 的分布式锁

    单节点 1.拉取镜像:docker pull zookeeper 2.运行容器 a.我的容器同一放在/root/docker下面,然后创建相应的目录和文件, mkdir zookeeper cd zo ...

  8. Java进阶专题(二十五) 分布式锁原理与实现

    前言 ​ 现如今很多系统都会基于分布式或微服务思想完成对系统的架构设计.那么在这一个系统中,就会存在若干个微服务,而且服务间也会产生相互通信调用.那么既然产生了服务调用,就必然会存在服务调用延迟或失败 ...

  9. zookeeper学习(2)----zookeeper和kafka的关系

    转载: Zookeeper 在 Kafka 中的作用 leader 选举 和 follower 信息同步 如上图所示,kafaka集群的 broker,和 Consumer 都需要连接 Zookeep ...

  10. kafka学习(二)-zookeeper集群搭建

    zookeeper概念 ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名 服务等.Zookeeper是h ...

随机推荐

  1. Vue源码分析之实现一个简易版的Vue

    目标 参考 https://cn.vuejs.org/v2/guide/reactivity.html 使用 Typescript 编写简易版的 vue 实现数据的响应式和基本的视图渲染,以及双向绑定 ...

  2. 第一篇Scrum冲刺博客

    目录 一.Alpha 阶段认领的任务 二.明日成员的任务安排 三.整个项目预期的任务量 四.敏捷开发前的感想 五.团队期望 一.Alpha 阶段认领的任务 陈起廷 任务 预计时间 日记天气.心情选择 ...

  3. IDEA常用快捷键Mac os和Windows对照--用到了就会更新

    之前公司用了一段的MacBookPro,离职后自己入手了一台MacBookPro.但是现在的公司中使用的电脑是古老的win7,两个系统的键盘有些许差别,而且快捷键也略有不同.最近因为疫情影响,在家远程 ...

  4. .Net MongoDB批量修改集合中子集合的字段

    环境:.Net Core 3.1 (需要导入.Net MongoDB的驱动) 模型 /// <summary> /// 收藏 /// </summary> public cla ...

  5. jQuery源码分析系列(三)Sizzle选择器引擎-下

    选择函数:select() 看到select()函数,if(match.length === 1){}存在的意义是尽量简化执行步骤,避免compile()函数的调用. 简化操作同样根据tokenize ...

  6. Java高级特性——反射机制(完结)——反射与注解

    按照我们的学习进度,在前边我们讲过什么是注解以及注解如何定义,如果忘了,可以先回顾一下https://www.cnblogs.com/hgqin/p/13462051.html. 在学习反射和注解前, ...

  7. 快速解决Ubuntu/linux 环境下QT生成没有可执行文件(application/x-executable)

    快速解决Ubuntu/linux 环境下QT生成没有可执行文件(application/x-executable)(转载)   问题描述 与windows环境下不同,linux选择debug构建时并不 ...

  8. 单元测试与单元测试框架 Jest

    什么是单元测试? 测试是一种验证我们的代码是否可以按预期工作的手段. 被测试的对象可以是我们程序的任何一个组成部分.大到一个分为多步骤的下单流程,小到代码中的一个函数. 单元测试特指被测试对象为程序中 ...

  9. 07.初步学习redis哨兵机制

    [ ] 一.哨兵(sentinal)的介绍 哨兵是redis集群架构中非常重要的一个组件,主要功能如下: 集群监控,负责监控redis master和slave进程是否正常工作 消息通知,如果某个re ...

  10. CRMEB小程序商城首页强制在微信中打开解决办法

    先说一下,这也算不上二开,小小修改一下而已. CRMEB安装完成后,PC端直接打开首页,真是一言难尽~ 然后,我就想了,用手机浏览器或者PC浏览器直接打开首页也没啥用,干脆直接强制在微信中打开算了! ...