zookeeper 分布式锁 java_Zookeeper实现分布式锁

Zookeeper实现分布式锁

我在一个简单的例子聊分布式锁中留了一个小尾巴,就是用Zookeeper(以下简称zk)实现分布式锁,今天就扫清这个尾巴。

实现原理

关于zk的知识点可以参考这篇文章:Zookeeper的功能以及工作原理,这里不做过多的介绍。这里介绍一下zk的涉及分布式锁的相关概念。

相关概念

有序节点:顾名思义就是有顺序的节点。zk会在生成节点时根据现有的节点数量添加整数序号。比如已经存在节点/lock/node-0000000000,下一个节点就是/lock/node-0000000001。

临时节点:临时节点只在zk会话期间存在,会话结束或超时时会被zk自动删除。

事件监听:通过zk的事件监听机制可以让客户端收到节点状态变化。主要的事件类型有节点数据变化、节点的删除和创建。

实现步骤

了解完上面的三个概念,下面介绍具体实现。

算法流程如下:

1、每个客户端创建临时有序节点

2、客户端获取节点列表,判断自己是否列表中的第一个节点,如果是就获得锁,如果不是就监听自己前面的节点,等待前面节点被删除。

3、如果获取锁就进行正常的业务流程,执行完释放锁。

上述步骤2中,有人可能担心如果节点发现自己不是序列最小的节点,准备添加监听器,但是这个时候前面节点正好被删除,这时候添加监听器是永远不起作用的,其实zk的API可以保证读取和添加监听器是一个原子操作。

为什么要监听前一个节点而不是所有的节点呢?这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。

Zookeeper 实现分布式锁的示意图如下:

91976b27a188

image.png

上图中左边是Zookeeper集群, lock是数据节点,node_1到node_n表示一系列的顺序临时节点,右侧client_1到client_n表示要获取锁的客户端。Service是互斥访问的服务。

代码实现

下面的源码是根据Zookeeper的开源客户端Curator实现分布式锁。采用zk的原生API实现会比较复杂,所以这里就直接用Curator这个轮子,采用Curator的acquire和release两个方法就能实现分布式锁。

import org.apache.curator.RetryPolicy;

import org.apache.curator.framework.CuratorFramework;

import org.apache.curator.framework.CuratorFrameworkFactory;

import org.apache.curator.framework.recipes.locks.InterProcessMutex;

import org.apache.curator.retry.ExponentialBackoffRetry;

public class CuratorDistributeLock {

public static void main(String[] args) {

RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

CuratorFramework client = CuratorFrameworkFactory.newClient("111.231.83.101:2181",retryPolicy);

client.start();

CuratorFramework client2 = CuratorFrameworkFactory.newClient("111.231.83.101:2181",retryPolicy);

client2.start();

//创建分布式锁, 锁空间的根节点路径为/curator/lock

InterProcessMutex mutex = new InterProcessMutex(client,"/curator/lock");

final InterProcessMutex mutex2 = new InterProcessMutex(client2,"/curator/lock");

try {

mutex.acquire();

} catch (Exception e) {

e.printStackTrace();

}

//获得了锁, 进行业务流程

System.out.println("clent Enter mutex");

Thread client2Th = new Thread(new Runnable() {

@Override

public void run() {

try {

mutex2.acquire();

System.out.println("client2 Enter mutex");

mutex2.release();

System.out.println("client2 release lock");

}catch (Exception e){

e.printStackTrace();

}

}

});

client2Th.start();

//完成业务流程, 释放锁

try {

Thread.sleep(5000);

mutex.release();

System.out.println("client release lock");

client2Th.join();

} catch (Exception e) {

e.printStackTrace();

}

//关闭客户端

client.close();

}

}

上述代码的执行结果如下:

91976b27a188

image.png

可以看到client客户端首先拿到锁再执行业务,然后再轮到client2尝试获取锁并执行业务。

源码分析

一直追踪acquire()的加锁方法,可以追踪到加锁的核心函数为attemptLock。

String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception

{

.....

while ( !isDone )

{

isDone = true;

try

{

//创建临时有序节点

ourPath = driver.createsTheLock(client, path, localLockNodeBytes);

//判断自己是否最小序号的节点,如果不是添加监听前面节点被删的通知

hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);

}

}

//如果获取锁返回节点路径

if ( hasTheLock )

{

return ourPath;

}

....

}

深入internalLockLoop函数源码:

private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception

{

.......

while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )

{

//获取子节点列表按照序号从小到大排序

List children = getSortedChildren();

String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash

//判断自己是否是当前最小序号节点

PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);

if ( predicateResults.getsTheLock() )

{

//成功获取锁

haveTheLock = true;

}

else

{

//拿到前一个节点

String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();

//如果没有拿到锁,调用wait,等待前一个节点删除时,通过回调notifyAll唤醒当前线程

synchronized(this)

{

try

{

//设置监听器,getData会判读前一个节点是否存在,不存在就会抛出异常从而不会设置监听器

client.getData().usingWatcher(watcher).forPath(previousSequencePath);

//如果设置了millisToWait,等一段时间,到了时间删除自己跳出循环

if ( millisToWait != null )

{

millisToWait -= (System.currentTimeMillis() - startMillis);

startMillis = System.currentTimeMillis();

if ( millisToWait <= 0 )

{

doDelete = true; // timed out - delete our node

break;

}

//等待一段时间

wait(millisToWait);

}

else

{

//一直等待下去

wait();

}

}

catch ( KeeperException.NoNodeException e )

{

//getData发现前一个子节点被删除,抛出异常

}

}

}

}

}

.....

}

总结

采用zk实现分布式锁在实际应用中不是很常见,需要一套zk集群,而且频繁监听对zk集群来说也是有压力,所以不推荐大家用。不能去面试的时候,能具体说一下使用zk实现分布式锁,我想应该也是一个加分项 🙂。

PS:距离上一篇技术文章间隔很久了,一方面是最近一直在忙着工作中的事情,另一方面还要准备找工作面试,所以已经很久没有更新技术文章。 原来我计划每个月写两篇,没想到落下这么多,忙完这上面的两件事,一定抓紧补上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值