Shared Reentrant Lock
在《Leader Election 的使用与分析》介绍过,Leader Election选主,是使用了一个InterProcessMutex
分布式锁来实现的。
而InterProcessMutex
就是这里要介绍的Shared Reentrant Lock:可重入共享分布式锁。
用于全局同步的完整分布式锁,意味着在同一个时间点,对于同一把锁,不会有两个持有者。即,同一时间点,同一把锁,最多只有一个持有者。
1. 关键API
org.apache.curator.framework.recipes.locks.InterProcessMutex
2. 机制说明
类名中倒是没有Lock的字样。
名字中的含义是:进程间互斥。这也体现着锁的本质。
3. 用法
3.1 创建
public InterProcessMutex(CuratorFramework client,
String path)
参数说明:
- client:zk客户端链接
- path:加锁的zk节点path
3.2 使用
InterProcessMutex实例是一个可重用的对象。不需要每次使用时创建一个新的实例。可以安全的使用单例
3.2.1 获得锁
如果想要获得锁,则需要在下列acquire方法中,选择一个调用:
1.永久阻塞
public void acquire()
此方法,会申请获得一个互斥锁。并且会一直阻塞,直到获得可用锁为止。
注意:同一个已持有锁的线程可以直接重入的。
每一此锁用完后都应该释放,也即是每一个acquire方法都应该有一个对应的release方法
2.限期阻塞
和上面的方法类似。区别在于,此方法不会永久阻塞,而是在超过等待期限后,返回是否获得锁的结果。
3.2.2 释放锁
public void release()
持有锁的线程如果调用此方法,就会释放持有的锁。 但是,要注意,如果线程多次调用了acquire方法,那么也应该调用相同次数的release方法,否则线程会继续持有锁
3.2.3 撤销
InterProcessMutex支持一种协商撤销互斥锁的机制。 可以用于死锁的情况
想要撤销一个互斥锁可以调用下面这个方法:
public void makeRevocable(RevocationListener<T> listener)
这个方法可以让锁持有者来处理撤销动作。 当其他进程/线程想要你释放锁时,就会回调参数中的监听器方法。 但是,此方法不是强制撤销的,是一种协商机制
当想要去撤销/释放一个锁时,可以通过Revoker
中的静态方法来发出请求:
public static void attemptRevoke(CuratorFramework client,
String path)
throws Exception
- path :加锁的zk节点path,通常可以通过
InterProcessMutex.getParticipantNodes()
获得
这个方法会发出撤销某个锁的请求。如果锁的持有者注册了上述的RevocationListener
监听器,那么就会调用监听器方法协商撤销锁。
4. 错误处理
和选主类似, 在实际使用中,必须考虑链接问题。 强烈建议:添加一个
ConnectionStateListener
用以处理链接中断或者丢失的情况
如果遇到链接中断
SUSPENDED
,在恢复链接RECONNECTED
之前,就不能保证是不是还持有锁了。 而如果链接丢失LOST
,那就意味着不再持有锁了。
5. 源码分析
5.1 类定义
先来看看类定义:
public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex>{}
实现了两个接口:
org.apache.curator.framework.recipes.locks.InterProcessLock
- 定义了锁操作的api
acquire
release
isAcquiredInThisProcess
- 定义了锁操作的api
org.apache.curator.framework.recipes.locks.Revocable
- 定义了可撤销锁的api
makeRevocable
- 定义了可撤销锁的api
5.2 成员变量
public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex>
{
private final LockInternals internals;
private final String basePath;
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
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;
}
}
private static final String LOCK_NAME = "lock-";
}
- internals
- final
org.apache.curator.framework.recipes.locks.LockInternals
- 内部锁对象的数据结构
- 持有者与锁有关的信息
- basePath
- final
- 加锁的zk节点path
- 会在这个path之下创建节点
- 所以是base path
- threadData
- final
java.util.concurrent.ConcurrentMap
- 记录着对同一个锁,竞争的多个本地线程
- LockData
- 私有静态内部类
- 存放在threadData中的数据对象
- owningThread 线程
- lockPath 加锁的path
- lockCount 加锁的计数器
- LOCK_NAME
- 私有常量
- 会在zk的
basePath
下建立一些名字上带有LOCK_NAME
的节点
5.2.1 LockInternals
基本上InterProcessMutex中没有定义什么信息,而很多信息都是在LockInternals
中。所以,有必要来看看它的源码:
public class LockInternals
{
private final CuratorFramework client;
private final String path;
private final String basePath;
private final LockInternalsDriver driver;
private final String lockName;
private final AtomicReference<RevocationSpec> revocable = new AtomicReference<RevocationSpec>(null);
private final CuratorWatcher revocableWatcher = new CuratorWatcher()
{
@Override
public void process(WatchedEvent event) throws Exception
{
if ( event.getType() == Watcher.Event.EventType.NodeDataChanged )
{
checkRevocableWatcher(event.getPath());
}
}
};
private final Watcher watcher = new Watcher()
{
@Override
public void process(WatchedEvent event)
{
notifyFromWatcher();
}
};
private volatile int maxLeases;
static final byte[] REVOKE_MESSAGE = "__REVOKE__".getBytes();
}
- client : zk客户端
- path : 加锁path
- basePath : 枷锁basePath
- driver
org.apache.curator.framework.recipes.locks.LockInternalsDriver
- 见 5.2.1.1 LockInternalsDriver
- lockName : 锁名称
- revocable
org.apache.curator.framework.recipes.locks.RevocationSpec
- 锁撤销动作所需的信息
- 撤销任务
- 撤销任务执行的线程池
- revocableWatcher
org.apache.curator.framework.api.CuratorWatcher
- 通过
checkRevocableWatcher
对锁是否撤销进行监听
- watcher
org.apache.zookeeper.Watcher
- zk原生监听器
- 通过
notifyFromWatcher
让本地线程重新竞争
- maxLeases
volatile
- 最大租约数
- REVOKE_MESSAGE
- 常量
- 锁撤销时,写入锁节点的内容
5.2.1.1 LockInternalsDriver
在LockInternals中,还有一个driver
,可以来看看这个“驱动”
package org.apache.curator.framework.recipes.locks;
import org.apache.curator.framework.CuratorFramework;
import java.util.List;
public interface LockInternalsDriver extends LockInternalsSorter
{
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception;
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception;
}
- 继承与
LockInternalsSorter
- 这个套路在Leader Latch中也见过
- 用于节点进行统一规则的排序
getsTheLock
- 获取锁
createsTheLock
- 创建锁
可以看出,锁的创建以及获取都是通过这个driver
来获得的。
5.3 构造器
public InterProcessMutex(CuratorFramework client, String path)
{
this(client, path, new StandardLockInternalsDriver());
}
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
{
this(client, path, LOCK_NAME, 1, driver);
}
InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver)
{
basePath = PathUtils.validatePath(path);
internals = new LockInternals(client, driver, path, lockName, maxLeases);
}
可以看见,InterProcessMutex的构造器比较简单。
- 规范化了path
- 然后初始化了
LockInternals
- 默认使用了
StandardLockInternalsDriver
; - 默认最大租约数为1,也就是一个zk节点对应着一把锁
5.3.1 StandardLockInternalsDriver
在InterProcessMutex构造器中,可以看见,默认是使用了一个StandardLockInternalsDriver
。
5.3.2 LockInternals
所以更多的细节都在LockInternals中:
LockInternals(CuratorFramework client, LockInternalsDriver driver, String path, String lockName, int maxLeases)
{
this.driver = driver;
this.lockName = lockName;
this.maxLeases = maxLeases;
this.client = client;
this.basePath = PathUtils.validatePath(path);
this.path = ZKPaths.makePath(path, lockName);
}
简单的赋值初始化
- 用path和lockName构建了正式的path
5.4 加锁
通过3.2节可以知道,加锁动作是由acquire
方法完成的。所以,来看看是如何加锁的。
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);
}
可以发现,不论是一直阻塞的方式,还是设置等待期限的方式,最终都是由internalLock
方法完成加锁动作的。
private boolean internalLock(long time, TimeUnit unit) 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 )
{
// 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;
}
- 从
threadData
(ConcurrentMap
)中,取出当前线程对应的LockData
对象 - 如果有,表示已经持有锁
- 在
lockData
上的计数器进行原子化递增 - 返回true,加锁成功
- 在
- 如果没有,表示当前线程还没有得到锁
- 调用
LockInternals
的attemptLock
方法,尝试加锁
- 如果得到锁
- 构建一个
LockData
,放入threadData
中 - 返回true,加锁成功
- 构建一个
- 如果没有得到锁,返回false
- 调用
这个方法也比较简单,只是将当前线程与锁进行绑定。而实际的加锁动作,是由LockInternals
的attemptLock
方法完成。
不过,注意一下方法中的注释。
Note on concurrency: a given lockData instance can be only acted on by a single thread so locking isn't necessary
对于指定的一个
lockData
实例,只能作用于一个线程,所以这个方法没有必要进行并发控制
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;
}
这个方法就比较复杂了。先来看看定义了哪些局部变量:
- startMillis : 启动时间
- millisToWait : 需要等待的时间长度
- 参数
unit
为空,则为null,永久
- 参数
- localLockNodeBytes
- 如果设置了锁撤销任务,则节点内容为0字节的数据
- 如果没有设置锁撤销任务,则写入参数
lockNodeBytes
- 实际上这个
lockNodeBytes
是个null
- 实际上这个
- retryCount : 重试次数
- ourPath : 持有的path
- hasTheLock : 是否持有锁
- isDone : 尝试加锁动作是否完成
用了一个“死循环”不断地尝试加锁,直到isDone
标记为true
- 标记
isDone
标记为true - 调用
driver
的createsTheLock
,创建锁对应的path - 调用
internalLockLoop
方法- 循环对第2步中的path进行加锁
- 如果过程中发现节点不存在了(例如:被删,session到期)
- 如果zk的重试策略允许重试
- 则阻塞,并尝试恢复
- 如果恢复成功,则继续循环,尝试加锁
- 如果恢复失败,则抛出异常,终止加锁操作
- 如果加锁成功,则返回锁对应的path
- 如果枷锁失败,则返回null
可以看出,加锁动作其实主要是两步:
- 调用
driver
的createsTheLock
,创建锁对应的path - 调用
internalLockLoop
方法
默认情况,driver
是org.apache.curator.framework.recipes.locks.StandardLockInternalsDriver
,所以,来看看它做了什么:
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
String ourPath;
if ( lockNodeBytes != null )
{
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
}
else
{
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
}
return ourPath;
}
其实,也比较简单。就是在path下创建一个临时有序节点。 lockNodeBytes
在InterProcessMutex
中,默认就是null
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;
}
终于到了真正干活的org.apache.curator.framework.recipes.locks.LockInternals#internalLockLoop
方法了。
定义了两个局部变量
- haveTheLock 是否加锁成功
- doDelete 进行time out的清理动作标识
- 为锁节点加上撤销监听器
- 只要链接状态正常,就一直尝试加锁
- 与Leader Latch类似,获取子节点,并对子节点进行排序
- 通过
driver
获得当前参与者信息 - 如果当前参与者持有锁,加锁成功,退出循环
- 否则开始尝试加锁
- 拼接好上一顺位参与者的path
- 使用
synchronized(this)
同步锁,保证加锁过程线程安全- 为上一顺位参与者添加监听器
- 如果设置了期限
- 则对等待时间重新计算
- 更新
doDelete
标识 - 如果已经逾期,则退出循环
- 使用
Object
的wait
阻塞
- 如果遇到未知异常,意外退出
- 发出线程中断信号
- 无论是否设置期限,都标记
doDelete
标识- 不论如何,意外退出就进行节点清理工作
- finally
- 如果
doDelete
,需要清理- 则调用
deleteOurPath
方法,对当前的path进行删除
- 则调用
- 如果
- 返回结果
可以注意到:
- 如果path在加锁过程中被删除,不论是误删除还是锁已经释放,也即是遇到
KeeperException.NoNodeException
时,是不做任何处理的。这个时候,只需要重新申请锁即可。 haveTheLock
结果来自于predicateResults.getsTheLock()
- 而
predicateResults
是由driver.getsTheLock
返回的
- 而
所以,还需要看看driver.getsTheLock
的处理逻辑:
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception
{
int ourIndex = children.indexOf(sequenceNodeName);
validateOurIndex(sequenceNodeName, ourIndex);
boolean getsTheLock = ourIndex < maxLeases;
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
return new PredicateResults(pathToWatch, getsTheLock);
}
- 获取当前参与者的顺位
- 检查当前参与者不在序列中(如:被误删,session同步),则抛出异常。
- 如果当前的顺位小于最大租约数,直接获得锁(在
InterProcessMutex
中,默认为1,也即是,只取1个候选人) - 如果没有获得锁,则设定监听的path为上一顺位的参与者节点
5.4.1 小结
-
整个加锁过程可以归纳为几个步骤:
- 计数器加一
- 参与者在base path下创建有序临时节点
- 获取base path下的所有参与者节点
- 对参与者进行排序
- 使用
driver
获得当前参与者的信息 - 如果当前参与者没有持有锁,则监听上一顺位的参与者,也即是,等待上一顺位释放锁
-
整个过程,就类似排队领赠品,每个人限领一份,领完还想要,就必须得重新排队
-
只要当前参与者排在第一顺位,则无论是第几次申请锁,都认为获得了锁,也即是可重入
5.5 释放锁
释放锁,需要调用release()
方法:
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);
}
}
- 获取当前线程对应的
lockData
- 对decrement的计数器减一
- 如果计数器不为零,也即是说,当前线程还有任务持有着锁,就不做处理,直接返回
- 对于持有锁的线程是可重入的
- 如果小于零,则抛出异常
- 计数器无意义
- 等于0,也即是当前线程真的不想再持有锁了
- 调用
LockInternals
的releaseLock
方法 - 将当前线程从
threadData
移除,即不在绑定当前线程
- 调用
- 同样,因为操作都是基于
lockData
,所以release
方法,也不需要同步处理
那么,可以发现,实质上的释放动作在LockInternals
的releaseLock
方法:
void releaseLock(String lockPath) throws Exception
{
revocable.set(null);
deleteOurPath(lockPath);
}
可以看到比较简单:
- 取消撤销动作
- 直接删除zk节点
5.5.1 小结
可以归纳一下锁撤销的几个步骤:
- 计数器减一
- 计数器归零时
- 取消撤销任务
- 删除ZK节点
- 解除当前线程的绑定
5.6 锁撤销
撤销动作是由org.apache.curator.framework.recipes.locks.Revocable
接口定义的。所以,可以看看InterProcessMutex
是如何实现这个接口的:
@Override
public void makeRevocable(RevocationListener<InterProcessMutex> listener)
{
makeRevocable(listener, MoreExecutors.sameThreadExecutor());
}
@Override
public void makeRevocable(final RevocationListener<InterProcessMutex> listener, Executor executor)
{
internals.makeRevocable(new RevocationSpec(executor, new Runnable()
{
@Override
public void run()
{
listener.revocationRequested(InterProcessMutex.this);
}
}));
}
可以看出,默认使用了SameThreadExecutorService
作为撤销任务的执行线程池。 这个线程池其实就是调用者的线程来执行任务。也即使申请锁的参与者自身的线程。
任务的执行最终注册到了LockInternals
类的revocable
属性上,这是一个原子引用包装的org.apache.curator.framework.recipes.locks.RevocationSpec
对象。
撤销任务,就是调用了回调了监听器的revocationRequested
方法。
撤销任务的触发,可以参见 5.4 加锁中,对于org.apache.curator.framework.recipes.locks.LockInternals#internalLockLoop
的介绍。
- 当revocable不为空时,会在数据节点上增加一个
revocableWatcher
监听器。 - 当数据节点发生变动时,这个监听器就会执行
org.apache.curator.framework.recipes.locks.LockInternals#checkRevocableWatcher
方法
private void checkRevocableWatcher(String path) throws Exception
{
RevocationSpec entry = revocable.get();
if ( entry != null )
{
try
{
byte[] bytes = client.getData().usingWatcher(revocableWatcher).forPath(path);
if ( Arrays.equals(bytes, REVOKE_MESSAGE) )
{
entry.getExecutor().execute(entry.getRunnable());
}
}
catch ( KeeperException.NoNodeException ignore )
{
// ignore
}
}
}
获取节点中的数据,判断是否是REVOKE_MESSAGE
的数据。如果是,则判定此锁已被撤销,于是执行撤销任务。
6. 测试
这只是一个简单的锁测试例子
并没有加入ConnectionStateListener
对链接状态的处理。 另外,对于分布式锁,在本地多线程之间传递,以及单线程内部重入的场景也没有测试。
有兴趣可以自己尝试
package com.roc.curator.demo.locks
import org.apache.commons.lang3.RandomStringUtils
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
import org.junit.Before
import org.junit.Test
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Created by roc on 2017/5/29.
*/
class InterProcessMutexTest {
val LATCH_PATH: String = "/test/locks/ipm"
var client: CuratorFramework = CuratorFrameworkFactory.builder()
.connectString("0.0.0.0:8888")
.connectionTimeoutMs(5000)
.retryPolicy(ExponentialBackoffRetry(1000, 10))
.sessionTimeoutMs(3000)
.build()
@Before fun init() {
client.start()
}
@Test fun runTest() {
var id: String = RandomStringUtils.randomAlphabetic(10)
println("id : $id ")
val time = Date()
var lock: InterProcessMutex = InterProcessMutex(client, LATCH_PATH)
var count: Int = 0;
while (true) {
if (lock.acquire(3, TimeUnit.SECONDS)) {
println("$id 加锁成功 $time")
while (lock.isAcquiredInThisProcess) {
println("$id 执行$count $time")
TimeUnit.SECONDS.sleep(2)
if (Math.random() > 0.89) {
println("$id 释放锁 $time")
lock.release()
}
}
count++
} else {
println("$id 加锁失败 $time")
println("竞争者:${lock.participantNodes}")
}
if (count > 10) {
break;
}
}
println("$id 结束: $time")
}
}
ls /test/locks/ipm
[
_c_3a44a050-50cf-466f-9970-d2cc77242ed1-lock-0000000061,
_c_8e5c71cf-c8bf-466b-b429-c7a0fc59eefb-lock-0000000064,
_c_f8952c4c-9cca-4f6d-a29c-f0f549c17e20-lock-0000000065
]
get /test/locks/ipm/_c_19cc648c-62e8-49d3-a465-86d6bb89005c-lock-0000000101
192.168.60.165
cZxid = 0x1defe
ctime = Mon May 29 14:55:40 CST 2017
mZxid = 0x1defe
mtime = Mon May 29 14:55:40 CST 2017
pZxid = 0x1defe
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x15156529fae07f8
dataLength = 14
numChildren = 0