curator 分布式锁InterProcessMutex
写这篇文章的目的主要是为了记录下自己在zookeeper 锁上踩过的坑,以及踩坑之后自己的一点认识;
从zk分布式锁原理说起,原理很简单,大家也应该都知道,简单的说就是zookeeper实现分布式锁是通过在zk集群上的路径实现的,在获取分布式锁的时候在zk服务器集群节点上创建临时顺序节点,释放锁的时候删除该临时节点.
多么简单的一句话,但是当你实现起来,想去做点优化的时候往往会变得很难,难的我们后续说;
再从需求说起,需求就是加锁,但是由于原来吞吐量不是很大,只是配置了一个固定的锁路径,但是却不是每次都会去根据这个锁路径创建锁,而是将这个锁路径存放在一个本地的HashMap中,这样的话,我就没有必要每次都去重复的创建这个锁对象,简单高效的利用;
变更后的需求是这样的,为了降低锁的力度,每次我要动态的生成一个path去zk上进行创建,然后再根据这个path生成锁对象,但是,一开始我依旧是沿用老的思维,想避免重复创建这个path的锁对象,于是,我想弄个三方缓存来存储这个锁对象,这时候坑就来了;
接下来,我们开始分析我的踩坑之旅:
public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex>
{
...
}
这是curator里面重入锁对象的结构,InterProcessLock
这个是curator通用的锁接口,定义的跟jdk本身的也差不多,也是curator留给开发者自己去定制实现符合自己业务需求的锁对象的;Revocable
接口是用来执行取消动作时触发动作用到的,如果你自定义锁对象的时候在释放锁对象时想触发一些动作,你可以实现它的方法,以上便是InterProcessLock结构的介绍;
看到这个代码结构我们还看出什么东西没?它并没有实现Serializable,导致其无法被序列化,也就是上面我自己想改进我业务中锁的场景就不支持了,因为类似于redis这种缓存,没法去存放一个对象,它顶多支持字符串以及byte[]
,所以我的想法就被loss掉了;
即使与业务无关了,但是我们作为可爱的程序员还是有必要去研究一下这个玩意的内部实现,因为我们不知道下次我们还会遇到什么场景,所以有必要让自己刻骨铭心一次;
接下来,我们看其内部实现,也就是我们高大上的源码之旅:
private final LockInternals internals;
private final String basePath;
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
internals:这个是所有申请锁与释放锁的核心实现,待会我们再来讲内部实现;
basePath:锁定的路径;
threadData:内部缓存锁的容器;
实现流程主要是这样的:每次初始化InterProcessMutex
对象的时候都会初始化一个StandardLockInternalsDriver
对象,这个对象我们后面再讲它的使用,同时也会初始化一个LockInternals对象,
接下来,我们来看获取锁的代码:
public void acquire() throws Exception{
if ( !internalLock(-1, null) ) {
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
private boolean internalLock(long time, TimeUnit unit) throws Exception {
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// re-entering
lockData.lockCount.incrementAndGet();
return true;
}
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
逻辑如下:
每次获取锁时会直接从本地缓存中先获取锁的元数据,如果存在,则在原有的计数器基础上+1,直接返回;
否则,尝试去获取锁,逻辑如下,
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
final long startMillis = System.currentTimeMillis();
//等待时间
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
int retryCount = 0;
String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;
while ( !isDone )
{
isDone = true;
try
{
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
catch ( KeeperException.NoNodeException e )
{
// gets thrown by StandardLockInternalsDriver when it can't find the lock node
// this can happen when the session expires, etc. So, if the retry allows, just try it all again
if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
{
isDone = false;
}
else
{
throw e;
}
}
}
if ( hasTheLock )
{
return ourPath;
}
return null;
}
首先设置一个是否有锁的标志hasTheLock = false
,然后
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
这个地方主要是通过StandardLockInternalsDriver
在锁目录下创建EPHEMERAL_SEQUENTIAL节点,
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
这里主要是循环获取锁的过程,代码看下面,首先是判断是否实现了revocable接口,如果实现了那么就对这个path设置监听,否则的话通过StandardLockInternalsDriver尝试得到PredicateResults(主要是否得到锁及需要监视的目录的两个属性);
private boolean internalLockLoop(long startMillis,Long millisToWait, String ourPath) throws Exception{
boolean haveTheLock = false;
boolean doDelete = false;
try{
if ( revocable.get() != null ){ client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
}
while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ){
List<String> 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();
synchronized(this)
{
try
{
// use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
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 )
{
// it has been deleted (i.e. lock released). Try to acquire again
}
}
}
}
}
catch ( Exception e )
{
ThreadUtils.checkInterrupted(e);
doDelete = true;
throw e;
}
finally
{
if ( doDelete )
{
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
然后判断PredicateResults中的pathToWatch(主要保存sequenceNode)是否是最小的节点,如果是,则得到锁,getsTheLock为true,否则得到该序列的前一个节点,设为pathToWatch,并监控起来;再判断获取锁的时间是否超时,超时则删除节点,不竞争下次锁,否则,睡眠等待获取锁;最后把获取的锁对象的锁路径等信息封装成LockData存储在本地缓存中.
获取锁的逻辑主要就是这些,有兴趣的同学可以打断点跟踪学习下,
下面是释放锁的过程;
public void release() throws Exception
{
/*
Note on concurrency: a given lockData instance
can be only acted on by a single thread so locking isn't necessary
*/
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData == null )
{
throw new IllegalMonitorStateException("You do not own the lock: " + basePath);
}
int newLockCount = lockData.lockCount.decrementAndGet();
if ( newLockCount > 0 )
{
return;
}
if ( newLockCount < 0 )
{
throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
}
try
{
internals.releaseLock(lockData.lockPath);
}
finally
{
threadData.remove(currentThread);
}
}
代码很简单,从本地缓存中拿到锁对象,计数器-1,只有到那个计数器=0的时候才会去执internals.releaseLock(lockData.lockPath);
final void releaseLock(String lockPath) throws Exception
{
client.removeWatchers();
revocable.set(null);
deleteOurPath(lockPath);
}
只要逻辑见名知意,首先移除watcher监听,这个监听可能是在循环获取锁的时候创建的,然后取消动作时触发动作时间置空,最后就是删除path;
最后做个小总结吧
- curator的InterProcessLock接口提供了多种锁机制,互斥锁,读写锁,以及可定时数的互斥锁的机制(这个大家具体问题具体分析).
- 所有申请锁都会创建临时顺序节点,保证了都能够有机会去获取锁.
- 内部用了线程的wait()和notifyAll()这种等待机制,可以及时的唤醒最渴望得到锁的线程.避免常规利用Thread.sleep()这种无用的间隔等待机制.
- 利用redis做锁的时候,一般都需要做锁的有效时间限定。而curator则利用了zookeeper的临时顺序节点特性,一旦客户端失去连接后,则就会自动清除该节点.
转自:https://www.jianshu.com/p/5fa6a1464076
curator 分布式锁InterProcessMutex的更多相关文章
- Zookeeper+Curator 分布式锁
本来想着基于zk临时节点,实现一下分布式锁,结果发现有curator框架.PS:原声API真的难用,连递归创建path都没有? 配置curator maven的时候,md配置了好几个小时,最后发现集中 ...
- zookeeper 笔记--curator分布式锁
使用ZK实现分布式独占锁, 原理就是利用ZK同级节点的唯一性. Curator框架下的一些分布式锁工具InterProcessMutex:分布式可重入排它锁 InterProcessSemaphore ...
- 05.Curator分布式锁
锁:分布式的锁全局同步,这意味着任何一个时间点不会有两个客户端都拥有相同的锁. 1.可重入锁Shared Reentrant Lock 首先我们先看一个全局可重入的锁(可以多次获取,不 ...
- zookeeper之分布式锁以及分布式计数器(通过curator框架实现)
有人可能会问zookeeper我知道,但是curator是什么呢? 其实curator是apachede针对zookeeper开发的一个api框架是apache的顶级项目 他与zookeeper原生a ...
- ZooKeeper(八)-- Curator实现分布式锁
1.pom.xml <dependencies> <dependency> <groupId>junit</groupId> <artifactI ...
- org.apache.curator:master选举和分布式锁
1. master选举(LeaderSelector) 1)LeaderSelector构造函数 在leaderPath上建立分布式锁:mutex = new InterProcessMutex(cl ...
- 女朋友也能看懂的Zookeeper分布式锁原理
前言 关于分布式锁,在互联网行业的使用场景还是比较多的,比如电商的库存扣减,秒杀活动,集群定时任务执行等需要进程互斥的场景.而实现分布式锁的手段也很多,大家比较常见的就是redis跟zookeep ...
- 使用ZooKeeper实现Java跨JVM的分布式锁(优化构思)
一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 说明 ...
- SpringBoot电商项目实战 — Zookeeper的分布式锁实现
上一篇演示了基于Redis的Redisson分布式锁实现,那今天我要再来说说基于Zookeeper的分布式现实. Zookeeper分布式锁实现 要用Zookeeper实现分布式锁,我就不得不说说zo ...
随机推荐
- mysql常用赋权命令
GRANT 所需权限 ON 库名.表名 TO '账号'@'允许访问的IP地址'; 把中文按需替换掉 所需权限:select.insert.delete.update按需要来. 上面那个允许访问IP ' ...
- 移除元素的golang实现
给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度. 不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成 ...
- [bug] 验证selenium的显式和隐式等待而发现的一个低级错误
隐式等待:如果在规定时间内网页加载完成,则执行下一步,否则一直等到时间截止,然后执行下一步.按照这说法举了个例子为啥不会按照预期执行了,难不成是这个定义有问题(~~~~~直接否定不是定义的问题,相信它 ...
- Luogu P4707 重返现世
题目描述 为了打开返回现世的大门,Yopilla 需要制作开启大门的钥匙.Yopilla 所在的迷失大陆有 \(n\) 种原料,只需要集齐任意 \(k\) 种,就可以开始制作. Yopilla 来到了 ...
- WPF之托盘图标的设定
首先需要在项目中引用System.Windows.Forms,System.Drawing; using System; using System.Collections.Generic; using ...
- swift的类型描述符
Metatype Types A concrete or existential metatype in SIL must describe its representation. This can ...
- Android应用启动、退出分析
http://www.jianshu.com/p/72059201b10a §AMS和应用进程 §应用启动流程 §应用退出流程 §启动.退出消息 AMS和应用进程 应用进程 <- 系统管理 &l ...
- Spring Boot开发Web应用
静态资源访问 在我们开发Web应用的时候,需要引用大量的js.css.图片等静态资源. 默认配置 Spring Boot默认提供静态资源目录位置需置于classpath下,目录名需符合如下规则: /s ...
- <数据结构与算法分析>读书笔记--要分析的问题
通常,要分析的最重要的资源就是运行时间.有几个因素影响着程序的运行时间.有些因素(如使用编译器和计算机)显然超出了任何理论模型的范畴,因此,虽然它们是重要的,但是我们在这里还是不能考虑它们.剩下的主要 ...
- sparse 稀疏函数的用法
sparse函数 功能:创建稀疏矩阵 用法1:S=sparse(X)—将矩阵X转化为稀疏矩阵的形式,即矩阵X中任何零元素去除,非零元素及其下标(索引)组成矩阵S. 如果X本身是稀疏的,sparse(X ...