目录
源码地址:https://gitee.com/pengzhang_master/curator-zookeeper-demo.git
基于 Curator 实现zookeeper的分布式锁
通过curator创建zookeeper的连接并启动,通过InterProcessMutex创建一个存放分布式锁临时节点的路径/locks,多线程模拟多个client在zookeeper上获取分布式锁。
public class LockDemo {
private static String CONNECTION_STR="123.206.34.40:2181";
public static void main(String[] args) throws Exception {
//创建连接并启动
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().
connectString(CONNECTION_STR).sessionTimeoutMs(50000000).
retryPolicy(new ExponentialBackoffRetry(1000, 3)).build();
curatorFramework.start();
final InterProcessMutex lock=new InterProcessMutex(curatorFramework,"/locks");
for(int i=0;i<10;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"->尝试竞争锁");
try {
lock.acquire(); //阻塞竞争锁
System.out.println(Thread.currentThread().getName()+"->成功获得了锁");
} catch (Exception e) {
e.printStackTrace();
}
try {
Thread.sleep(400000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
lock.release(); //释放锁
} catch (Exception e) {
e.printStackTrace();
}
}
},"Thread-"+i).start();
}
}
通过lock.acquire(),可以在/locks节点下创建临时有序节点,并进行阻塞。
当发现自己创建的临时节点是所有临时有序节点中序列最小的,则获取分布式锁,并进行自己的业务逻辑。
当业务逻辑执行完成之后则通过lock.release()释放锁,删除自己的临时节点。
其他没有获取锁的客户端会监听自己节点的上一个节点,若监听到上一个节点被删除,则获取锁,执行业务逻辑。
源码解析
分布式锁
在上面的例子中,通过new InterProcessMutex(curatorFramework,"/locks")构造InterProcessMutex对象。
public InterProcessMutex(CuratorFramework client, String path)
{
this(client, path, new StandardLockInternalsDriver());
}
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
{
//maxLeases=1 表示可以获取分布式锁的线程数(客户端数)为1,为互斥锁
this(client, path, LOCK_NAME, 1, driver);
}
InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver)
{
basePath = PathUtils.validatePath(path);
//InterProcessMutex 将分布式锁的申请和释放操作委托给internals 执行
internals = new LockInternals(client, driver, path, lockName, maxLeases);
}
InterProcessMutex.acquire,创建临时节点并竞争锁,此时线程阻塞,当竞争到锁之后,执行业务逻辑,再通过lock.release()释放锁。
@Override
public void acquire() throws Exception
{
//time为-1,是无线等待
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
*/
Thread currentThread = Thread.currentThread();
//在threadData中获取线程信息
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// re-entering
//同一线程再次 acquire,首先判断当前的映射表内(threadData)是否有该线程的锁信息,如果有则原子+1,然后返回
lockData.lockCount.incrementAndGet();
return true;
}
// 映射表内没有对应的锁信息,尝试通过LockInternals.attemptLock获取锁
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
// 成功获取锁,记录信息到映射表
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
LockInternals.attemptLock,尝试获取锁,并返回锁对应的 Zookeeper 临时顺序节点的路径
//尝试获取锁,并返回锁对应的 Zookeeper 临时顺序节点的路径
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;
//在 Zookeeper 中创建的临时顺序节点的路径,相当于一把待激活的分布式锁
//激活条件:同级目录子节点,名称排序最小
String ourPath = null;
// 是否已经持有分布式锁
boolean hasTheLock = false;
// 是否已经完成尝试获取分布式锁的操作
boolean isDone = false;
while ( !isDone )
{
isDone = true;
try
{
// 在Zookeeper中创建临时顺序节点
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;
}
driver.createsTheLock,创建临时节点,并返回节点路径。
// 在 Zookeeper 中创建临时顺序节点
@Override
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
String ourPath;
// lockNodeBytes 不为 null 则作为数据节点内容,否则采用默认内容(IP 地址)
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;
}
LockInternals.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 )
{
// 获取排序后的子节点列表
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;
}
driver.getsTheLock,获取分布式锁。
@Override
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception
{
//获取自己创建的临时节点在排序后子节点列表中的索引
int ourIndex = children.indexOf(sequenceNodeName);
// 校验之前创建的临时顺序节点是否有效
validateOurIndex(sequenceNodeName, ourIndex);
//判断自己创建的临时节点的索引是否小于1(maxLeases)
boolean getsTheLock = ourIndex < maxLeases;
//如果小于1,则说明自己所对应的临时节点就是最小的,获得锁;如果不是,则返回比自己小的上一个节点,为了减少羊群效应或惊群效应
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
// 返回获取锁的结果,交由上层继续处理(添加监听等操作)
return new PredicateResults(pathToWatch, getsTheLock);
}
释放锁
通过InterProcessMutex.release来释放锁。
@Override
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();
//如果大于0,则说明还需要再次释放锁
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);
}
}