导读

  • 真是有人()的地方就有江湖(事务),今天不谈江湖,来撩撩人。

  • 分布式锁的概念、为什么使用分布式锁,想必大家已经很清楚了。前段时间作者写过Redis是如何实现分布式锁,今天这篇文章来谈谈Zookeeper是如何实现分布式锁的。

  • 陈某今天分别从如下几个方面来详细讲讲ZK如何实现分布式锁:

    1. ZK的四种节点

    2. 排它锁的实现

    3. 读写锁的实现

    4. Curator实现分步式锁

ZK的四种节点

  • 持久性节点:节点创建后将会一直存在

  • 临时节点:临时节点的生命周期和当前会话绑定,一旦当前会话断开临时节点也会删除,当然可以主动删除。

  • 持久有序节点:节点创建一直存在,并且zk会自动为节点加上一个自增的后缀作为新的节点名称。

  • 临时有序节点:保留临时节点的特性,并且zk会自动为节点加上一个自增的后缀作为新的节点名称。

排它锁的实现

  • 排他锁的实现相对简单一点,利用了zk的创建节点不能重名的特性。如下图:

  • 根据上图分析大致分为如下步骤:

    1. 尝试获取锁:创建临时节点,zk会保证只有一个客户端创建成功。

    2. 创建临时节点成功,获取锁成功,执行业务逻辑,业务执行完成后删除锁。

    3. 创建临时节点失败,阻塞等待。

    4. 监听删除事件,一旦临时节点删除了,表示互斥操作完成了,可以再次尝试获取锁。

    5. 递归:获取锁的过程是一个递归的操作,获取锁->监听->获取锁

  • 如何避免死锁:创建的是临时节点,当服务宕机会话关闭后临时节点将会被删除,锁自动释放。

代码实现

  • 作者参照JDK锁的实现方式加上模板方法模式的封装,封装接口如下:

/**
* @Description ZK分布式锁的接口
* @Author 陈某
* @Date 2020/4/7 22:52
*/
public interface ZKLock {
/**
* 获取锁
*/
void lock() throws Exception;

/**
* 解锁
*/
void unlock() throws Exception;
}
  • 模板抽象类如下:

/**
* @Description 排他锁,模板类
* @Author 陈某
* @Date 2020/4/7 22:55
*/
public abstract class AbstractZKLockMutex implements ZKLock {

/**
* 节点路径
*/
protected String lockPath;

/**
* zk客户端
*/
protected CuratorFramework zkClient;

private AbstractZKLockMutex(){}

public AbstractZKLockMutex(String lockPath,CuratorFramework client){
this.lockPath=lockPath;
this.zkClient=client;
}

/**
* 模板方法,搭建的获取锁的框架,具体逻辑交于子类实现
* @throws Exception
*/
@Override
public final void lock() throws Exception {
//获取锁成功
if (tryLock()){
System.out.println(Thread.currentThread().getName()+"获取锁成功");
}else{ //获取锁失败
//阻塞一直等待
waitLock();
//递归,再次获取锁
lock();
}
}

/**
* 尝试获取锁,子类实现
*/
protected abstract boolean tryLock() ;


/**
* 等待获取锁,子类实现
*/
protected abstract void waitLock() throws Exception;


/**
* 解锁:删除节点或者直接断开连接
*/
@Override
public abstract void unlock() throws Exception;
}
  • 排他锁的具体实现类如下:

/**
* @Description 排他锁的实现类,继承模板类 AbstractZKLockMutex
* @Author 陈某
* @Date 2020/4/7 23:23
*/
@Data
public class ZKLockMutex extends AbstractZKLockMutex {

/**
* 用于实现线程阻塞
*/
private CountDownLatch countDownLatch;

public ZKLockMutex(String lockPath,CuratorFramework zkClient){
super(lockPath,zkClient);
}

/**
* 尝试获取锁:直接创建一个临时节点,如果这个节点存在创建失败抛出异常,表示已经互斥了,
* 反之创建成功
* @throws Exception
*/
@Override
protected boolean tryLock() {
try {
zkClient.create()
//临时节点
.withMode(CreateMode.EPHEMERAL)
//权限列表 world:anyone:crdwa
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(lockPath,"lock".getBytes());
return true;
}catch (Exception ex){
return false;
}
}


/**
* 等待锁,一直阻塞监听
* @return 成功获取锁返回true,反之返回false
*/
@Override
protected void waitLock() throws Exception {
//监听节点的新增、更新、删除
final NodeCache nodeCache = new NodeCache(zkClient, lockPath);
//启动监听
nodeCache.start();
ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();

//监听器
NodeCacheListener listener=()-> {
//节点被删除,此时获取锁
if (nodeCache.getCurrentData() == null) {
//countDownLatch不为null,表示节点存在,此时监听到节点删除了,因此-1
if (countDownLatch != null)
countDownLatch.countDown();
}
};
//添加监听器
listenable.addListener(listener);

//判断节点是否存在
Stat stat = zkClient.checkExists().forPath(lockPath);
//节点存在
if (stat!=null){
countDownLatch=new CountDownLatch(1);
//阻塞主线程,监听
countDownLatch.await();
}
//移除监听器
listenable.removeListener(listener);
}

/**
* 解锁,直接删除节点
* @throws Exception
*/
@Override
public void unlock() throws Exception {
zkClient.delete().forPath(lockPath);
}
}

可重入性排他锁如何设计

  • 可重入的逻辑很简单,在本地保存一个ConcurrentMapkey是当前线程,value是定义的数据,结构如下:

 private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
  • 重入的伪代码如下:

public boolean tryLock(){
//判断当前线程是否在threadData保存过
//存在,直接return true
//不存在执行获取锁的逻辑
//获取成功保存在threadData中
}

读写锁的实现

  • 读写锁分为读锁和写锁,区别如下:

    • 读锁允许多个线程同时读数据,但是在读的同时不允许写线程修改。

    • 写锁在获取后,不允许多个线程同时写或者读。

  • 如何实现读写锁?ZK中有一类节点叫临时有序节点,上文有介绍。下面我们来利用临时有序节点来实现读写锁的功能。

读锁的设计

  • 读锁允许多个线程同时进行读,并且在读的同时不允许线程进行写操作,实现原理如下图:

  • 根据上图,获取一个读锁分为以下步骤:

    1. 创建临时有序节点(当前线程拥有的读锁或称作读节点)。

    2. 获取路径下所有的子节点,并进行从小到大排序

    3. 获取当前节点前的临近写节点(写锁)。

    4. 如果不存在的临近写节点,则成功获取读锁。

    5. 如果存在临近写节点,对其监听删除事件。

    6. 一旦监听到删除事件,重复2,3,4,5的步骤(递归)

写锁的设计

  • 线程一旦获取了写锁,不允许其他线程读和写。实现原理如下:

  • 从上图可以看出唯一和写锁不同的就是监听的节点,这里是监听临近节点(读节点或者写节点),读锁只需要监听写节点,步骤如下:

    1. 创建临时有序节点(当前线程拥有的写锁或称作写节点)。

    2. 获取路径下的所有子节点,并进行从小到大排序。

    3. 获取当前节点的临近节点(读节点和写节点)。

    4. 如果不存在临近节点,则成功获取锁。

    5. 如果存在临近节点,对其进行监听删除事件。

    6. 一旦监听到删除事件,重复2,3,4,5的步骤(递归)

如何监听

  • 无论是写锁还是读锁都需要监听前面的节点,不同的是读锁只监听临近的写节点,写锁是监听临近的所有节点,抽象出来看其实是一种链式的监听,如下图:

  • 每一个节点都在监听前面的临近节点,一旦前面一个节点删除了,再从新排序后监听前面的节点,这样递归下去。

代码实现

  • 作者简单的写了读写锁的实现,先造出来再优化,不建议用在生产环境。代码如下:

public class ZKLockRW  {

/**
* 节点路径
*/
protected String lockPath;

/**
* zk客户端
*/
protected CuratorFramework zkClient;

/**
* 用于阻塞线程
*/
private CountDownLatch countDownLatch=new CountDownLatch(1);


private final static String WRITE_NAME="_W_LOCK";

private final static String READ_NAME="_R_LOCK";


public ZKLockRW(String lockPath, CuratorFramework client) {
this.lockPath=lockPath;
this.zkClient=client;
}

/**
* 获取锁,如果获取失败一直阻塞
* @throws Exception
*/
public void lock() throws Exception {
//创建节点
String node = createNode();
//阻塞等待获取锁
tryLock(node);
countDownLatch.await();
}

/**
* 创建临时有序节点
* @return
* @throws Exception
*/
private String createNode() throws Exception {
//创建临时有序节点
return zkClient.create()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(lockPath);
}

/**
* 获取写锁
* @return
*/
public ZKLockRW writeLock(){
return new ZKLockRW(lockPath+WRITE_NAME,zkClient);
}

/**
* 获取读锁
* @return
*/
public ZKLockRW readLock(){
return new ZKLockRW(lockPath+READ_NAME,zkClient);
}

private void tryLock(String nodePath) throws Exception {
//获取所有的子节点
List<String> childPaths = zkClient.getChildren()
.forPath("/")
.stream().sorted().map(o->"/"+o).collect(Collectors.toList());


//第一个节点就是当前的锁,直接获取锁。递归结束的条件
if (nodePath.equals(childPaths.get(0))){
countDownLatch.countDown();
return;
}

//1. 读锁:监听最前面的写锁,写锁释放了,自然能够读了
if (nodePath.contains(READ_NAME)){
//查找临近的写锁
String preNode = getNearWriteNode(childPaths, childPaths.indexOf(nodePath));
if (preNode==null){
countDownLatch.countDown();
return;
}
NodeCache nodeCache=new NodeCache(zkClient,preNode);
nodeCache.start();
ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();
listenable.addListener(() -> {
//节点删除事件
if (nodeCache.getCurrentData()==null){
//继续监听前一个节点
String nearWriteNode = getNearWriteNode(childPaths, childPaths.indexOf(preNode));
if (nearWriteNode==null){
countDownLatch.countDown();
return;
}
tryLock(nearWriteNode);
}
});
}

//如果是写锁,前面无论是什么锁都不能读,直接循环监听上一个节点即可,直到前面无锁
if (nodePath.contains(WRITE_NAME)){
String preNode = childPaths.get(childPaths.indexOf(nodePath) - 1);
NodeCache nodeCache=new NodeCache(zkClient,preNode);
nodeCache.start();
ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();
listenable.addListener(() -> {
//节点删除事件
if (nodeCache.getCurrentData()==null){
//继续监听前一个节点
tryLock(childPaths.get(childPaths.indexOf(preNode) - 1<0?0:childPaths.indexOf(preNode) - 1));
}
});
}
}

/**
* 查找临近的写节点
* @param childPath 全部的子节点
* @param index 右边界
* @return
*/
private String getNearWriteNode(List<String> childPath,Integer index){
for (int i = 0; i < index; i++) {
String node = childPath.get(i);
if (node.contains(WRITE_NAME))
return node;

}
return null;
}

}

Curator实现分步式锁

  • Curator是Netflix公司开源的一个Zookeeper客户端,与Zookeeper提供的原生客户端相比,Curator的抽象层次更高,简化了Zookeeper客户端的开发量。

  • Curator在分布式锁方面已经为我们封装好了,大致实现的思路就是按照作者上述的思路实现的。中小型互联网公司还是建议直接使用框架封装好的,毕竟稳定,有些大型的互联公司都是手写的,牛逼啊。

  • 创建一个排他锁很简单,如下:

//arg1:CuratorFramework连接对象,arg2:节点路径
lock=new InterProcessMutex(client,path);
//获取锁
lock.acquire();
//释放锁
lock.release();
  • 更多的API请参照官方文档,不是此篇文章重点。

  • 至此ZK实现分布式锁就介绍完了,如有想要源码的朋友,老规矩,关注微信公众号【码猿技术专栏】,回复关键词分布式锁获取。

一点小福利

  • 对于Zookeeper不太熟悉的朋友,陈某特地花费两天时间总结了ZK的常用知识点,包括ZK常用shell命令、ZK权限控制、Curator的基本操作API。目录如下:

  • 需要上面PDF文件的朋友,老规矩,关注微信公众号【码猿技术专栏】回复关键词ZK总结

求你了,别再问我Zookeeper如何实现分布式锁了!!!的更多相关文章

  1. 如何用Zookeeper来实现分布式锁?

    什么是Zookeeper临时顺序节点? 例如 : / 动物 植物 猫 仓鼠 荷花 松树 Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Zonde.# Znode分为四种类型 ...

  2. 基于zookeeper实现的分布式锁

    基于zookeeper实现的分布式锁 2011-01-27 • 技术 • 7 条评论 • jiacheo •14,941 阅读 A distributed lock base on zookeeper ...

  3. java使用zookeeper实现的分布式锁示例

    java使用zookeeper实现的分布式锁示例 作者: 字体:[增加 减小] 类型:转载 时间:2014-05-07我要评论 这篇文章主要介绍了java使用zookeeper实现的分布式锁示例,需要 ...

  4. 求你了,再问你Java内存模型的时候别再给我讲堆栈方法区了…

    GitHub 4.1k Star 的Java工程师成神之路 ,不来了解一下吗? GitHub 4.1k Star 的Java工程师成神之路 ,真的不来了解一下吗? GitHub 4.1k Star 的 ...

  5. zookeeper 笔记--curator分布式锁

    使用ZK实现分布式独占锁, 原理就是利用ZK同级节点的唯一性. Curator框架下的一些分布式锁工具InterProcessMutex:分布式可重入排它锁 InterProcessSemaphore ...

  6. 基于Zookeeper实现多进程分布式锁

    一.zookeeper简介及基本操作 Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化.当对目录节点监控状态打开时,一旦目录节点的状态发生变化,Watc ...

  7. 基于zookeeper简单实现分布式锁

    https://blog.csdn.net/desilting/article/details/41280869 这里利用zookeeper的EPHEMERAL_SEQUENTIAL类型节点及watc ...

  8. 基于zookeeper实现高性能分布式锁

    实现原理:利用zookeeper的持久性节点和Watcher机制 具体步骤: 1.创建持久性节点 zkLock 2.在此父节点下创建子节点列表,name按顺序定义 3.Java程序获取该节点下的所有顺 ...

  9. Zookeeper怎么实现分布式锁?

    对访问资源 R1 的过程加锁,在操作 O1 结束对资源 R1 访问前,其他操作不允许访问资源 R1.以上算是对独占锁的简单定义了,那么这段定义在 Zookeeper 的"类 Unix/Lin ...

随机推荐

  1. vijos 1449 字符串还原

    背景 小K是一位蔚蓝教主的崇拜者(Orz教主er),有一天,他收到了一封匿名信,信告诉了小K由于他表现出色,得到了一次当面Orz教主的机会,但是要当面Orz教主可不那么容易,不是每个人都有资格Orz教 ...

  2. 关于使用Binlog和canal来对MySQL的数据写入进行监控

    先说下Binlog和canal是什么吧. 1.Binlog是mysql数据库的操作日志,当有发生增删改查操作时,就会在data目录下生成一个log文件,形如mysql-bin.000001,mysql ...

  3. Java探针技术-retransformclasses的介绍

    retransformclasses void retransformclasses(class... classes) throws unmodifiableclassexception 重转换提供 ...

  4. 【原创】Java并发编程系列1:大纲

    [原创]Java并发编程系列1:大纲 一个人能力当中所蕴藏的潜能,远超过自己想象以外. 为什么要学习并发编程 随着现今互联网行业的迅猛发展,其业务复杂度.并发量也在不断增加,对程序的要求变得越来越高, ...

  5. Spring MVC启动流程分析

    本文是Spring MVC系列博客的第一篇,后续会汇总成贴子. Spring MVC是Spring系列框架中使用频率最高的部分.不管是Spring Boot还是传统的Spring项目,只要是Web项目 ...

  6. java-随机点名器(新手)

    //创建的一个包名. package qige; //导入一个包.import java.util.Random; //定义一个类.public class Zy1 { //公共静态的主方法. pub ...

  7. docker-ce 在windows10下使用volume的注意事项

    最近想搭建一套CI/CD环境尝试一下,因为手里云服务太小了(1C1G),撑不起来gitlab和jenkins.恰巧年前配了台高配版的windows机器,就想在家里的机器上通过docker装gitlab ...

  8. 从一个小例子引发的Java内存可见性的简单思考和猜想以及DCL单例模式中的volatile的核心作用

    环境 OS Win10 CPU 4核8线程 IDE IntelliJ IDEA 2019.3 JDK 1.8 -server模式 场景 最初的代码 一个线程A根据flag的值执行死循环,另一个线程B只 ...

  9. shell编程之字符串处理

    # .#号截取,删除左边字符,保留右边字符,*// 表示从左边开始删除第一个 // 号及左边的所有字符 echo ${var#*//} # . ## 号截取,删除左边字符,保留右边字符,##*/ 表示 ...

  10. SSI服务器端包含注入

    服务器端嵌入:Server Side Include,是一种类似于ASP的基于服务器的网页制作技术.大多数(尤其是基于Unix平台)的WEB服务器如Netscape Enterprise Server ...