curator是Netflix公司开源的一个ZooKeeper客户端封装。curator 很好的实现了分布式锁,curator 提供了InterProcessMutex 这样一个 api。除了分布式锁之外,还提供了 leader 选举、分布式队列等常用的功能。org.apache.curator.framework.recipes.locks包下。
Curator的几种锁方案:
1 InterProcessMutex:分布式可重入排它锁
2 InterProcessSemaphoreMutex:分布式排它锁
3 InterProcessReadWriteLock:分布式读写锁
4 InterProcessMultiLock:将多个锁作为单个实体管理的容器
1 分布式锁要解决的问题
1 锁释放:
使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
2 阻塞锁
使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
3 可重入锁
使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
4 单点问题
使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
5 超时问题
2 InterProcessLock接口
public interface InterProcessLock
{
/**
* Acquire the mutex - blocking until it's available. Each call to acquire must be balanced by a call
* to {@link #release()}
*
* @throws Exception ZK errors, connection interruptions
*/
public void acquire() throws Exception;
/**
* Acquire the mutex - blocks until it's available or the given time expires. Each call to acquire that returns true must be balanced by a call
* to {@link #release()}
*
* @param time time to wait
* @param unit time unit
* @return true if the mutex was acquired, false if not
* @throws Exception ZK errors, connection interruptions
*/
public boolean acquire(long time, TimeUnit unit) throws Exception;
/**
* Perform one release of the mutex.
*
* @throws Exception ZK errors, interruptions
*/
public void release() throws Exception;
/**
* Returns true if the mutex is acquired by a thread in this JVM
*
* @return true/false
*/
boolean isAcquiredInThisProcess();
}
实现InterProcessLock接口的类:
1 InterProcessMultiLock 将多个锁作为单个实体管理的容器
2 InterProcessMutex 分布式可重入排它锁
3 InterProcessSemaphoreMutex 分布式排它锁
3 InterProcessMutex
分布式可重入排它锁
public InterProcessMutex(CuratorFramework client, String path)
{
this(client, path, new StandardLockInternalsDriver());
}
/**
* @param client client
* @param path the path to lock
* @param driver lock driver
*/
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
{
this(client, path, LOCK_NAME, 1, driver);
}
LockInternalsDriver 可自定义lock驱动实现分布式锁
3.1 内部成员
// 申请锁与释放锁的核心实现,zk节点创建删除等
private final LockInternals internals;
// 锁路径
private final String basePath;
// 内部缓存锁的容器;
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
private static final String LOCK_NAME = "lock-";
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;
}
}
3.2 获得锁acquire
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
{
/*
Note on concurrency: a given lockData instance
can be only acted on by a single thread so locking isn't necessary
*/
//1 尝试从缓存中获取,成功则重入,计数加1
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// re-entering
lockData.lockCount.incrementAndGet();
return true;
}
//2 通过internals获得锁
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
// 2.1 放入缓存
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
org.apache.curator.framework.recipes.locks.LockInternals#attemptLock实现
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
// 1状态基本信息
//记录当前时间
final long startMillis = System.currentTimeMillis();
//记录锁等待时间
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
//临时顺序节点的data
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
//节点不存在重试次数
int retryCount = 0;
//临时顺序节点path
String ourPath = null;
//当前path是否获取到锁
boolean hasTheLock = false;
//flag
boolean isDone = false;
// 2 循环获得锁
while ( !isDone )
{
isDone = true;
try
{
//1 创建临时顺序节点
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
//2 获取锁
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;
}
}
}
//获取到锁,返回path
if ( hasTheLock )
{
return ourPath;
}
return null;
}
//createsTheLock方法
@Override
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
String ourPath;
if ( lockNodeBytes != null )
{
ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
}
else
{
ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
}
return ourPath;
}
// internalLockLoop方法 锁阻塞和唤醒的核心实现
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 )
{
//获取到所有的子节点并排序 zk api sync 保证最新
List<String> children = getSortedChildren();
//当前节点名称
String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash
//1 判断是否可以拿到锁,前一个path:当前path是否获取到锁
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
//2 如果拿到锁,返回
if ( predicateResults.getsTheLock() )
{
haveTheLock = true;
}
else
{
//前一个path
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
//3 小于自己的节点增加watcher,节点状态变更时,为了获得唤醒当前线程,实现就是notifyAll,
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
{
//4 没有获得锁,阻塞当前线程
wait();
}
}
// 上一个节点已被删除 重试
catch ( KeeperException.NoNodeException e )
{
// it has been deleted (i.e. lock released). Try to acquire again
}
}
}
}
}
catch ( Exception e )
{
//异常场景删除此次创建的节点
doDelete = true;
throw e;
}
finally
{
if ( doDelete )
{
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
4 InterProcessSemaphoreMutex
是一种不可重入的分布式互斥锁。
5 InterProcessMultiLock
6 InterProcessReadWriteLock
分布式不可重入读写锁 没有实现InterProcessLock接口
1 可重入
此锁允许读取器和写入器都以可重入锁的样式重新获取读或写锁。在释放写线程/进程所拥有的所有写锁之前,不允许非重入型读取器。此外,写者可以获得读锁,反之则不能。如果读者试图获取写锁,它将永远不会成功。
2 锁降级
重新进入还可以通过获取写锁,然后读锁和释放写锁的方式,从写锁降级为读锁。但是,无法从读取锁升级到写入锁。
官方文档:http://curator.apache.org/curator-recipes/shared-reentrant-read-write-lock.html
javaDoc:http://curator.apache.org/apidocs/org/apache/curator/framework/recipes/locks/InterProcessReadWriteLock.html
6.1 分布式写锁基本原理:
如下图所示,可以将临时有序节点分为读锁节点和写锁节点:
1 对于读锁节点而言,其只需要关心前一个写锁节点的释放。如果前一个写锁释放了,则多个读锁节点对应的线程可以并发地读取数据;
2 对于写锁节点而言,其只需要关心前一个节点的释放,而不需要关心前一个节点是写锁节点还是读锁节点。因为为了保证有序性,写操作必须要等待前面的读操作或者写操作执行完成。
7 InterProcessSemaphoreV2
InterProcessSemaphoreV2 信号量(InterProcessSemaphore已废弃) 没有实现InterProcessLock接口。基于令牌桶算法,当一个线程要执行的时候就去桶里面获取令牌,如果有足够的令牌那么我就执行如果没有那么我就阻塞,当线程执行完毕也要将令牌放回桶里。
官方文档:http://curator.apache.org/curator-recipes/shared-semaphore.html
参考
1 https://www.jianshu.com/p/5fa6a1464076