CuratorZooKeeper的一个客户端框架,其中封装了分布式互斥锁的实现,最为常用的是InterProcessMutex,本文将对其进行代码剖析

简介

InterProcessMutex基于Zookeeper实现了分布式的公平可重入互斥锁,类似于单个JVM进程内的ReentrantLock(fair=true)

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 最常用
public InterProcessMutex(CuratorFramework client, String path){
// Zookeeper利用path创建临时顺序节点,实现公平锁的核心
this(client, path, new StandardLockInternalsDriver());
} public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver){
// maxLeases=1,表示可以获得分布式锁的线程数量(跨JVM)为1,即为互斥锁
this(client, path, LOCK_NAME, 1, driver);
} // protected构造函数
InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver){
basePath = PathUtils.validatePath(path);
// internals的类型为LockInternals,InterProcessMutex将分布式锁的申请和释放操作委托给internals执行
internals = new LockInternals(client, driver, path, lockName, maxLeases);
}

获取锁

InterProcessMutex.acquire

1
2
3
4
5
6
7
8
9
10
11
// 无限等待
public void acquire() throws Exception{
if ( !internalLock(-1, null) ){
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
} // 限时等待
public boolean acquire(long time, TimeUnit unit) throws Exception{
return internalLock(time, unit);
}

InterProcessMutex.internalLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private boolean internalLock(long time, TimeUnit unit) throws Exception{
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData != null ){
// 实现可重入
// 同一线程再次acquire,首先判断当前的映射表内(threadData)是否有该线程的锁信息,如果有则原子+1,然后返回
lockData.lockCount.incrementAndGet();
return true;
} // 映射表内没有对应的锁信息,尝试通过LockInternals获取锁
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
2
3
// 映射表
// 记录线程与锁信息的映射关系
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
1
2
3
4
5
6
7
8
9
10
11
12
// 锁信息
// Zookeeper中一个临时顺序节点对应一个“锁”,但让锁生效激活需要排队(公平锁),下面会继续分析
private static class LockData{
final Thread owningThread;
final String lockPath;
final AtomicInteger lockCount = new AtomicInteger(1); // 分布式锁重入次数 private LockData(Thread owningThread, String lockPath){
this.owningThread = owningThread;
this.lockPath = lockPath;
}
}

LockInternals.attemptLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 尝试获取锁,并返回锁对应的Zookeeper临时顺序节点的路径
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception{
final long startMillis = System.currentTimeMillis();
// 无限等待时,millisToWait为null
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
// 创建ZNode节点时的数据内容,无关紧要,这里为null,采用默认值(IP地址)
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
// 当前已经重试次数,与CuratorFramework的重试策略有关
int retryCount = 0; // 在Zookeeper中创建的临时顺序节点的路径,相当于一把待激活的分布式锁
// 激活条件:同级目录子节点,名称排序最小(排队,公平锁),后续继续分析
String ourPath = null;
// 是否已经持有分布式锁
boolean hasTheLock = false;
// 是否已经完成尝试获取分布式锁的操作
boolean isDone = false; while ( !isDone ){
isDone = true;
try{
// 从InterProcessMutex的构造函数可知实际driver为StandardLockInternalsDriver的实例
// 在Zookeeper中创建临时顺序节点
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
// 循环等待来激活分布式锁,实现锁的公平性,后续继续分析
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
} catch ( KeeperException.NoNodeException e ) {
// 容错处理,不影响主逻辑的理解,可跳过
// 因为会话过期等原因,StandardLockInternalsDriver因为无法找到创建的临时顺序节点而抛出NoNodeException异常
if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++,
System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) ){
// 满足重试策略尝试重新获取锁
isDone = false;
} else {
// 不满足重试策略则继续抛出NoNodeException
throw e;
}
}
}
if ( hasTheLock ){
// 成功获得分布式锁,返回临时顺序节点的路径,上层将其封装成锁信息记录在映射表,方便锁重入
return ourPath;
}
// 获取分布式锁失败,返回null
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// From StandardLockInternalsDriver
// 在Zookeeper中创建临时顺序节点
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception{
String ourPath;
// lockNodeBytes不为null则作为数据节点内容,否则采用默认内容(IP地址)
if ( lockNodeBytes != null ){
// 下面对CuratorFramework的一些细节做解释,不影响对分布式锁主逻辑的解释,可跳过
// creatingParentContainersIfNeeded:用于创建父节点,如果不支持CreateMode.CONTAINER
// 那么将采用CreateMode.PERSISTENT
// withProtection:临时子节点会添加GUID前缀
ourPath = client.create().creatingParentContainersIfNeeded()
// CreateMode.EPHEMERAL_SEQUENTIAL:临时顺序节点,Zookeeper能保证在节点产生的顺序性
// 依据顺序来激活分布式锁,从而也实现了分布式锁的公平性,后续继续分析
.withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
} else {
ourPath = client.create().creatingParentContainersIfNeeded()
.withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
}
return ourPath;
}

LockInternals.internalLockLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 循环等待来激活分布式锁,实现锁的公平性
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);
// 实现锁的公平性的核心逻辑,看下面的分析
PredicateResults predicateResults = driver.getsTheLock(client,
children , sequenceNodeName , maxLeases);
if (predicateResults.getsTheLock()) {
// 获得了锁,中断循环,继续返回上层
haveTheLock = true;
} else {
// 没有获得到锁,监听上一临时顺序节点
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized (this) {
try {
// exists()会导致导致资源泄漏,因此exists()可以监听不存在的ZNode,因此采用getData()
// 上一临时顺序节点如果被删除,会唤醒当前线程继续竞争锁,正常情况下能直接获得锁,因为锁是公平的
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
if (millisToWait != null) {
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if (millisToWait <= 0) {
doDelete = true; // 获取锁超时,标记删除之前创建的临时顺序节点
break;
}
wait(millisToWait); // 等待被唤醒,限时等待
} else {
wait(); // 等待被唤醒,无限等待
}
} catch (KeeperException.NoNodeException e) {
// 容错处理,逻辑稍微有点绕,可跳过,不影响主逻辑的理解
// client.getData()可能调用时抛出NoNodeException,原因可能是锁被释放或会话过期(连接丢失)等
// 这里并没有做任何处理,因为外层是while循环,再次执行driver.getsTheLock时会调用validateOurIndex
// 此时会抛出NoNodeException,从而进入下面的catch和finally逻辑,重新抛出上层尝试重试获取锁并删除临时顺序节点
}
}
}
}
} catch (Exception e) {
ThreadUtils.checkInterrupted(e);
// 标记删除,在finally删除之前创建的临时顺序节点(后台不断尝试)
doDelete = true;
// 重新抛出,尝试重新获取锁
throw e;
} finally {
if (doDelete) {
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// From StandardLockInternalsDriver
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception{
// 之前创建的临时顺序节点在排序后的子节点列表中的索引
int ourIndex = children.indexOf(sequenceNodeName);
// 校验之前创建的临时顺序节点是否有效
validateOurIndex(sequenceNodeName, ourIndex);
// 锁公平性的核心逻辑
// 由InterProcessMutex的构造函数可知,maxLeases为1,即只有ourIndex为0时,线程才能持有锁,或者说该线程创建的临时顺序节点激活了锁
// Zookeeper的临时顺序节点特性能保证跨多个JVM的线程并发创建节点时的顺序性,越早创建临时顺序节点成功的线程会更早地激活锁或获得锁
boolean getsTheLock = ourIndex < maxLeases;
// 如果已经获得了锁,则无需监听任何节点,否则需要监听上一顺序节点(ourIndex-1)
// 因为锁是公平的,因此无需监听除了(ourIndex-1)以外的所有节点,这是为了减少羊群效应,非常巧妙的设计!!
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
// 返回获取锁的结果,交由上层继续处理(添加监听等操作)
return new PredicateResults(pathToWatch, getsTheLock);
} static void validateOurIndex(String sequenceNodeName, int ourIndex) throws KeeperException{
if ( ourIndex < 0 ){
// 容错处理,可跳过
// 由于会话过期或连接丢失等原因,该线程创建的临时顺序节点被Zookeeper服务端删除,往外抛出NoNodeException
// 如果在重试策略允许范围内,则进行重新尝试获取锁,这会重新重新生成临时顺序节点
// 佩服Curator的作者将边界条件考虑得如此周到!
throw new KeeperException.NoNodeException("Sequential path not found: " + sequenceNodeName);
}
}
1
2
3
4
5
6
7
8
9
10
// From LockInternals
private final Watcher watcher = new Watcher(){
@Override
public void process(WatchedEvent event){
notifyFromWatcher();
}
};
private synchronized void notifyFromWatcher(){
notifyAll(); // 唤醒所有等待LockInternals实例的线程
}
1
2
3
4
5
6
7
8
9
10
// From LockInternals
private void deleteOurPath(String ourPath) throws Exception{
try{
// 后台不断尝试删除
client.delete().guaranteed().forPath(ourPath);
} catch ( KeeperException.NoNodeException e ) {
// 已经删除(可能会话过期导致),不做处理
// 实际使用Curator-2.12.0时,并不会抛出该异常
}
}

释放锁

弄明白了获取锁的原理,释放锁的逻辑就很清晰了

InterProcessMutex.release

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void release() throws Exception{
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 ){
// 锁是可重入的,初始值为1,原子-1到0,锁才释放
return;
}
if ( newLockCount < 0 ){
// 理论上无法执行该路径
throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
}
try{
// lockData != null && newLockCount == 0,释放锁资源
internals.releaseLock(lockData.lockPath);
} finally {
// 最后从映射表中移除当前线程的锁信息
threadData.remove(currentThread);
}
}

LockInternals.releaseLock

1
2
3
4
5
void releaseLock(String lockPath) throws Exception{
revocable.set(null);
// 删除临时顺序节点,只会触发后一顺序节点去获取锁,理论上不存在竞争,只排队,非抢占,公平锁,先到先得
deleteOurPath(lockPath);
}
1
2
3
4
5
6
7
8
9
10
// Class:LockInternals
private void deleteOurPath(String ourPath) throws Exception{
try{
// 后台不断尝试删除
client.delete().guaranteed().forPath(ourPath);
} catch ( KeeperException.NoNodeException e ) {
// 已经删除(可能会话过期导致),不做处理
// 实际使用Curator-2.12.0时,并不会抛出该异常
}
}

总结

InterProcessMutex的特性

  1. 分布式锁(基于Zookeeper
  2. 互斥锁
  3. 公平锁(监听上一临时顺序节点 + wait() / notifyAll()
  4. 可重入

基于Zookeeper实现的分布式互斥锁 - InterProcessMutex的更多相关文章

  1. 基于(Redis | Memcache)实现分布式互斥锁

    设计一个缓存系统,不得不要考虑的问题就是:缓存穿透.缓存击穿与失效时的雪崩效应. 缓存击穿 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则 ...

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

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

  3. 单机Redis实现分布式互斥锁

    代码地址如下:http://www.demodashi.com/demo/12520.html 0.准备工作 0-1 运行环境 jdk1.8 gradle 一个能支持以上两者的代码编辑器,作者使用的是 ...

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

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

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

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

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

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

  7. 【连载】redis库存操作,分布式锁的四种实现方式[一]--基于zookeeper实现分布式锁

    一.背景 在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以 ...

  8. ZooKeeper 分布式锁 Curator 源码 04:分布式信号量和互斥锁

    前言 分布式信号量,之前在 Redisson 中也介绍过,Redisson 的信号量是将计数维护在 Redis 中的,那现在来看一下 Curator 是如何基于 ZooKeeper 实现信号量的. 使 ...

  9. 基于ZooKeeper的分布式锁和队列

    在分布式系统中,往往需要一些分布式同步原语来做一些协同工作,上一篇文章介绍了Zookeeper的基本原理,本文介绍下基于Zookeeper的Lock和Queue的实现,主要代码都来自Zookeeper ...

随机推荐

  1. MapReduce 计算模式

    声明:本文摘录自<大数据日知录——架构与算法>一书. 较常见的计算模式有4类,实际应用中大部分ETL任务都可以归结为这些计算模式或者变体. 1.求和模式 a.数值求和 比如我们熟悉的单词计 ...

  2. boost 学习(1)

    智能指针的学习 中文教程网站 http://zh.highscore.de/cpp/boost/ 不过代码可能 由于BOOST 版本不同需要稍作修改 scoped_ptr 离开作用域则自动调用类析构函 ...

  3. 13.8.8 div块 居中

    <div style="border:1px solid blue;width:760px; height:410px; position:absolute; left:50%; to ...

  4. tensorflow的transpose

    从图中看出来perm=[1,0,2] 表示第一个维度和第二个维度进行交换. 默认的是[0,1,2]   所以perm=[1,0,2] 表示第一个维度和第二个维度进行交换.0,1,2表示index.

  5. WebService安全加密

    众所周知,WebService访问API是公开的,知道其URL者均可以研究与调用.那么,在只允许注册用户的WebService应用中,如何确保API访问和通信的安全性呢?本文所指的访问与通信安全性包括 ...

  6. memcached 连接本地问题

    刚开始学memcache ,就遇到一个问题. telnet 127.0.0.1 11211   回车之后就什么都没有提示了.然后不管设置什么都是报error . 表示不知道如何解决!先写个文章记录下来 ...

  7. 着重基础之—MySql Blob类型和Text类型

    着重基础之—MySql Blob类型和Text类型 在经历了几个Java项目后,遇到了一些问题,在解决问题中体会到基础需要不断的回顾与巩固. 最近做的项目中,提供给接口调用方数据同步接口,传输的数据格 ...

  8. Django入门与实践-第26章:个性化工具(完结)

    http://127.0.0.1:8000/boards/1/topics/62/reply/ 我觉得只添加内置的个性化(humanize)包就会很不错. 它包含一组为数据添加“人性化(human t ...

  9. Jsp+servlet+mysql搭建套路

    1.建立数据库根据需求建立相应的数据库确立数据库的字段.属性.主键等2.建立javaweb项目,搭建开发环境在开发环境的/WebRoot/WEB-INF下建立lib文件夹,存放需要使用的jar包常用的 ...

  10. 日期 时间选择器(DatePicker和TimePicker)实现用户选择

    日期和时间 作者的设计TimePicker时,大小分布不合理,我调整宽度为match-parent高度为wrap-parent就可以了. public class MainActivity exten ...