Curator分布式锁的使用分析

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
  • org.apache.curator.framework.recipes.locks.Revocable
    • 定义了可撤销锁的api
      • makeRevocable

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的构造器比较简单。

  1. 规范化了path
  2. 然后初始化了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;
}
  1. threadData(ConcurrentMap)中,取出当前线程对应的LockData对象
  2. 如果有,表示已经持有锁
    • lockData上的计数器进行原子化递增
    • 返回true,加锁成功
  3. 如果没有,表示当前线程还没有得到锁
    • 调用LockInternalsattemptLock方法,尝试加锁
    1. 如果得到锁
      • 构建一个LockData,放入threadData
      • 返回true,加锁成功
    2. 如果没有得到锁,返回false

这个方法也比较简单,只是将当前线程与锁进行绑定。而实际的加锁动作,是由LockInternalsattemptLock方法完成。

不过,注意一下方法中的注释。

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

  1. 标记isDone标记为true
  2. 调用drivercreatesTheLock,创建锁对应的path
  3. 调用internalLockLoop方法
    • 循环对第2步中的path进行加锁
  4. 如果过程中发现节点不存在了(例如:被删,session到期)
    1. 如果zk的重试策略允许重试
    2. 则阻塞,并尝试恢复
    3. 如果恢复成功,则继续循环,尝试加锁
    4. 如果恢复失败,则抛出异常,终止加锁操作
  5. 如果加锁成功,则返回锁对应的path
  6. 如果枷锁失败,则返回null

可以看出,加锁动作其实主要是两步:

  1. 调用drivercreatesTheLock,创建锁对应的path
  2. 调用internalLockLoop方法

默认情况,driverorg.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下创建一个临时有序节点。 lockNodeBytesInterProcessMutex中,默认就是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的清理动作标识
  1. 为锁节点加上撤销监听器
  2. 只要链接状态正常,就一直尝试加锁
    1. Leader Latch类似,获取子节点,并对子节点进行排序
    2. 通过driver获得当前参与者信息
    3. 如果当前参与者持有锁,加锁成功,退出循环
    4. 否则开始尝试加锁
      1. 拼接好上一顺位参与者的path
      2. 使用synchronized(this)同步锁,保证加锁过程线程安全
        1. 为上一顺位参与者添加监听器
        2. 如果设置了期限
          • 则对等待时间重新计算
          • 更新doDelete标识
          • 如果已经逾期,则退出循环
        3. 使用Objectwait阻塞
  3. 如果遇到未知异常,意外退出
    1. 发出线程中断信号
    2. 无论是否设置期限,都标记doDelete标识
      • 不论如何,意外退出就进行节点清理工作
  4. finally
    1. 如果doDelete,需要清理
      1. 则调用deleteOurPath方法,对当前的path进行删除
  5. 返回结果

可以注意到:

  • 如果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);
}
  1. 获取当前参与者的顺位
  2. 检查当前参与者不在序列中(如:被误删,session同步),则抛出异常。
  3. 如果当前的顺位小于最大租约数,直接获得锁(在InterProcessMutex中,默认为1,也即是,只取1个候选人)
  4. 如果没有获得锁,则设定监听的path为上一顺位的参与者节点

5.4.1 小结

  • 整个加锁过程可以归纳为几个步骤:

    1. 计数器加一
    2. 参与者在base path下创建有序临时节点
    3. 获取base path下的所有参与者节点
    4. 对参与者进行排序
    5. 使用driver获得当前参与者的信息
    6. 如果当前参与者没有持有锁,则监听上一顺位的参与者,也即是,等待上一顺位释放锁
  • 整个过程,就类似排队领赠品,每个人限领一份,领完还想要,就必须得重新排队

  • 只要当前参与者排在第一顺位,则无论是第几次申请锁,都认为获得了锁,也即是可重入

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);
    }
}
  1. 获取当前线程对应的lockData
  2. 对decrement的计数器减一
  3. 如果计数器不为零,也即是说,当前线程还有任务持有着锁,就不做处理,直接返回
    • 对于持有锁的线程是可重入的
  4. 如果小于零,则抛出异常
    • 计数器无意义
  5. 等于0,也即是当前线程真的不想再持有锁了
    1. 调用LockInternalsreleaseLock方法
    2. 将当前线程从threadData移除,即不在绑定当前线程
  • 同样,因为操作都是基于lockData,所以release方法,也不需要同步处理

那么,可以发现,实质上的释放动作在LockInternalsreleaseLock方法:

void releaseLock(String lockPath) throws Exception
{
    revocable.set(null);
    deleteOurPath(lockPath);
}

可以看到比较简单:

  1. 取消撤销动作
  2. 直接删除zk节点

5.5.1 小结

可以归纳一下锁撤销的几个步骤:

  1. 计数器减一
  2. 计数器归零时
    1. 取消撤销任务
    2. 删除ZK节点
    3. 解除当前线程的绑定

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值