[Curator] Shared Lock 的使用与分析

Shared Lock

Shared Reentrant Lock类似,不过不能重入

一个完整的分布式锁,意味着在同一个时间点,对于同一把锁,不会有两个持有者。即,同一时间点,同一把锁,最多只有一个持有者。

1. 关键 API

org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex

2. 机制说明

类名中同样没有Lock的字样。

名字中的含义是:进程间信号量互斥。

Shared Lock实际上就是一个对租约管理进行了定制化的Shared Reentrant Lock。

3. 用法

使用方式与Shared Reentrant Lock一样,这里就不再赘述了。

4. 错误处理

也与Shared Reentrant Lock一样,这里就不再赘述了。

5. 源码分析

5.1 类定义

先来看看类定义:

public class InterProcessSemaphoreMutex implements InterProcessLock{}
  • 仅实现了org.apache.curator.framework.recipes.locks.InterProcessLock接口
    • 定义了锁操作的api
      • acquire
      • release
      • isAcquiredInThisProcess

5.2 成员变量

public class InterProcessSemaphoreMutex implements InterProcessLock
{
    private final InterProcessSemaphoreV2 semaphore;
    private volatile Lease lease;
}
  • semaphore
    • final
    • 信号量
    • org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2
    • 管理着一把锁在进程间的租约
  • lease
    • volatile
    • org.apache.curator.framework.recipes.locks.Lease
    • 租约
    • 表示从semaphore中,获得的一份租约
5.2.1 InterProcessSemaphoreV2

InterProcessSemaphoreMutex内部操作逻辑,大量依赖于InterProcessSemaphoreV2,所以,有必要来看看这个类:

public class InterProcessSemaphoreV2
{
    private final Logger log = LoggerFactory.getLogger(getClass());
    private final InterProcessMutex lock;
    private final CuratorFramework client;
    private final String leasesPath;
    private final Watcher watcher = new Watcher()
    {
        @Override
        public void process(WatchedEvent event)
        {
            notifyFromWatcher();
        }
    };

    private volatile byte[] nodeData;
    private volatile int maxLeases;

    private static final String LOCK_PARENT = "locks";
    private static final String LEASE_PARENT = "leases";
    private static final String LEASE_BASE_NAME = "lease-";
    public static final Set<String> LOCK_SCHEMA = Sets.newHashSet(
            LOCK_PARENT,
            LEASE_PARENT
    );
}
  • log
  • lock
    • final
    • org.apache.curator.framework.recipes.locks.InterProcessMutex
  • client : ZK客户端
  • leasesPath
    • final
    • 租约对应的zk节点path
  • watcher
    • 监听器
  • nodeData
    • volatile
    • 节点中写入的数据
  • maxLeases
    • volatile
    • 最大组约数
  • LOCK_PARENT
    • 私有常量
  • LEASE_PARENT
    • 私有常量
  • LEASE_BASE_NAME
    • 私有常量
  • LOCK_SCHEMA
    • 公有常量

可以发现InterProcessSemaphoreV2内部,竟然还有着一个InterProcessMutex(Shared Reentrant Lock)

这里就可以发现,==Shared Lock==实际上就是一个对租约管理进行了定制化的==Shared Reentrant Lock==。

5.3 构造器

只有一个:

public InterProcessSemaphoreMutex(CuratorFramework client, String path)
{
    this.semaphore = new InterProcessSemaphoreV2(client, path, 1);
}

其实就是为了初始化一个最大租约数为1,不使用org.apache.curator.framework.recipes.shared.SharedCountReaderInterProcessSemaphoreV2

5.3.1 InterProcessSemaphoreV2
public InterProcessSemaphoreV2(CuratorFramework client, String path, int maxLeases)
{
    this(client, path, maxLeases, null);
}

public InterProcessSemaphoreV2(CuratorFramework client, String path, SharedCountReader count)
{
    this(client, path, 0, count);
}

private InterProcessSemaphoreV2(CuratorFramework client, String path, int maxLeases, SharedCountReader count)
{
    this.client = client;
    path = PathUtils.validatePath(path);
    lock = new InterProcessMutex(client, ZKPaths.makePath(path, LOCK_PARENT));
    this.maxLeases = (count != null) ? count.getCount() : maxLeases;
    leasesPath = ZKPaths.makePath(path, LEASE_PARENT);

    if ( count != null )
    {
        count.addListener
            (
                new SharedCountListener()
                {
                    @Override
                    public void countHasChanged(SharedCountReader sharedCount, int newCount) throws Exception
                    {
                        InterProcessSemaphoreV2.this.maxLeases = newCount;
                        notifyFromWatcher();
                    }

                    @Override
                    public void stateChanged(CuratorFramework client, ConnectionState newState)
                    {
                        // no need to handle this here - clients should set their own connection state listener
                    }
                }
            );
    }
}
  • 初始化了成员变量
  • 初始化了分布式锁
  • 如果使用了SharedCountReader模式,则会添加一个计数器的监听器
    • Shared Lock使用的是maxLeases模式,所以,这里不会添加监听器

5.4 加锁

通过3.2节可以知道,加锁动作是由acquire方法完成的。所以,来看看是如何加锁的。

public void acquire() throws Exception
{
    lease = semaphore.acquire();
}

public boolean acquire(long time, TimeUnit unit) throws Exception
{
    Lease acquiredLease = semaphore.acquire(time, unit);
    if ( acquiredLease == null )
    {
        return false;   // important - don't overwrite lease field if couldn't be acquired
    }
    lease = acquiredLease;
    return true;
}

比较简单的逻辑,本质上就是去申请信号量的过程。

  • 从构造器中可以发现,这个信号量的最大租约数只有1,所以自然的,就成了一个不可重入的锁实现了
  • 所有的逻辑都是在semaphore中实现的
5.4.1 InterProcessSemaphoreV2

再开看看这个信号量是如何申请的:

public Lease acquire() throws Exception
{
    Collection<Lease> leases = acquire(1, 0, null);
    return leases.iterator().next();
}

public Collection<Lease> acquire(int qty) throws Exception
{
    return acquire(qty, 0, null);
}

public Lease acquire(long time, TimeUnit unit) throws Exception
{
    Collection<Lease> leases = acquire(1, time, unit);
    return (leases != null) ? leases.iterator().next() : null;
}

public Collection<Lease> acquire(int qty, long time, TimeUnit unit) throws Exception
{
    long startMs = System.currentTimeMillis();
    boolean hasWait = (unit != null);
    long waitMs = hasWait ? TimeUnit.MILLISECONDS.convert(time, unit) : 0;

    Preconditions.checkArgument(qty > 0, "qty cannot be 0");

    ImmutableList.Builder<Lease> builder = ImmutableList.builder();
    boolean success = false;
    try
    {
        while ( qty-- > 0 )
        {
            int retryCount = 0;
            long startMillis = System.currentTimeMillis();
            boolean isDone = false;
            while ( !isDone )
            {
                switch ( internalAcquire1Lease(builder, startMs, hasWait, waitMs) )
                {
                    case CONTINUE:
                    {
                        isDone = true;
                        break;
                    }

                    case RETURN_NULL:
                    {
                        return null;
                    }

                    case RETRY_DUE_TO_MISSING_NODE:
                    {
                        // gets thrown by internalAcquire1Lease 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()) )
                        {
                            throw new KeeperException.NoNodeException("Sequential path not found - possible session loss");
                        }
                        // try again
                        break;
                    }
                }
            }
        }
        success = true;
    }
    finally
    {
        if ( !success )
        {
            returnAll(builder.build());
        }
    }

    return builder.build();
}

可以看到,InterProcessSemaphoreV2有4个acquire方法。本质上所有逻辑都是由最后一个acquire(int qty, long time, TimeUnit unit)实现,其他3个都是一个模板而已。所以,重点来看看这个方法的逻辑。

先来看看这个方法的javadoc是如何说的:

Acquire qty leases. If there are not enough leases available, this method blocks until either the maximum number of leases is increased enough or other clients/processes close enough leases. However, this method will only block to a maximum of the time parameters given. If time expires before all leases are acquired, the subset of acquired leases are automatically closed.

The client must close the leases when it is done with them. You should do this in a finally block. NOTE: You can use returnAll(Collection) for this.

用于申请获得qty个信号量。如果没有足够的可用信号量,这个方法会阻塞,等待可用的最大数量增大到足够的时候,或者,其他的客户端/进程释放了足够多的信号量的时候。然后,此方法,不会永久阻塞等待,可以通过参数指定等待的最大期限。 在到期时,如果没有获得足够的信号量,那么对于已分配的信号量也会全部释放。

对于获得信号量的客户端,必须在完成处理后主动释放。应该在finally代码块中完成释放动作,可以调用returnAll(Collection)方法完成释放。

回到源码部分,来看看,是如何实现上述逻辑的: 定义了几个局部变量:

  • startMs : 开始时间
  • hasWait : 是否有等待期限
  • waitMs : 按参数单位的时间长度,转换为毫秒数
  1. 定义了一个不可变List来存放申请到的信号量租约。com.google.common.collect.ImmutableList.Builder
  2. 只要没有足够qty个信号量时,一直重复申请动作
  3. 每一轮申请(每一个信号量的申请)都会初始化几个变量
    • retryCount :重试次数
    • startMillis : 此轮申请的开始时间
    • isDone : 是否完成
    1. 只要没完成,就一直尝试
      • 使用了一个状态机的套路
      1. 根据internalAcquire1Lease的返回状态进行下一步动作的依据
        • CONTINUE
          • 继续
          • 本轮申请完成,可以开始下一轮
        • RETURN_NULL
          • 申请失败
          • 返回 null
        • RETRY_DUE_TO_MISSING_NODE
          • 节点信息错误
            • 如,链接断开,session失效等情况
          • 如果还未超时,则重试
          • 如果已超时,则抛出KeeperException.NoNodeException
  4. 如果没有没有全部成功
    • 则finally代码块,会清理掉已建立的信号量

可以发现,对于单个信号量的申请动作,实际是由internalAcquire1Lease(builder, startMs, hasWait, waitMs)方法完成的,所以来看看这个方法做了哪些事:

private InternalAcquireResult internalAcquire1Lease(ImmutableList.Builder<Lease> builder, long startMs, boolean hasWait, long waitMs) throws Exception
{
    if ( client.getState() != CuratorFrameworkState.STARTED )
    {
        return InternalAcquireResult.RETURN_NULL;
    }

    if ( hasWait )
    {
        long thisWaitMs = getThisWaitMs(startMs, waitMs);
        if ( !lock.acquire(thisWaitMs, TimeUnit.MILLISECONDS) )
        {
            return InternalAcquireResult.RETURN_NULL;
        }
    }
    else
    {
        lock.acquire();
    }

    Lease lease = null;

    try
    {
        PathAndBytesable<String> createBuilder = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL);
        String path = (nodeData != null) ? createBuilder.forPath(ZKPaths.makePath(leasesPath, LEASE_BASE_NAME), nodeData) : createBuilder.forPath(ZKPaths.makePath(leasesPath, LEASE_BASE_NAME));
        String nodeName = ZKPaths.getNodeFromPath(path);
        lease = makeLease(path);

        if ( debugAcquireLatch != null )
        {
            debugAcquireLatch.await();
        }

        synchronized(this)
        {
            for(;;)
            {
                List<String> children;
                try
                {
                    children = client.getChildren().usingWatcher(watcher).forPath(leasesPath);
                }
                catch ( Exception e )
                {
                    if ( debugFailedGetChildrenLatch != null )
                    {
                        debugFailedGetChildrenLatch.countDown();
                    }
                    returnLease(lease); // otherwise the just created ZNode will be orphaned causing a dead lock
                    throw e;
                }
                if ( !children.contains(nodeName) )
                {
                    log.error("Sequential path not found: " + path);
                    returnLease(lease);
                    return InternalAcquireResult.RETRY_DUE_TO_MISSING_NODE;
                }

                if ( children.size() <= maxLeases )
                {
                    break;
                }
                if ( hasWait )
                {
                    long thisWaitMs = getThisWaitMs(startMs, waitMs);
                    if ( thisWaitMs <= 0 )
                    {
                        returnLease(lease);
                        return InternalAcquireResult.RETURN_NULL;
                    }
                    wait(thisWaitMs);
                }
                else
                {
                    wait();
                }
            }
        }
    }
    finally
    {
        lock.release();
    }
    builder.add(Preconditions.checkNotNull(lease));
    return InternalAcquireResult.CONTINUE;
}
  1. 首先判断zk客户端的状态,如果不正确直接放回RETURN_NULL
    • 申请失败
  2. 使用内部的分布式锁进行加锁
    • 根据是否等待来决定调用哪一个加锁方法
    • 如果在设定的期限内加锁失败,则放回RETURN_NULL
      • 申请失败
  3. 获得锁之后,再创建一个临时有序节点,用于信号量记录
  4. 调用makeLease将上一步的节点path包装成Lease对象
    • 之后操作Lease实质上就是操作第3步创建的这个临时有序节点
  5. synchronized加同步锁
    1. 不停重试判断此信号量是否可用
      1. 获取信号量节点列表
      2. 如果列表中不包含当前的信号量节点
        • 说明当前session失效,或者zk节点被误删等等
        • 返回RETRY_DUE_TO_MISSING_NODE需要重新申请了
      3. 如果列表数量小于等于maxleases
        • 说明申请的信号量是可用的啦
        • 退出循环
      4. 如果需要等待,则计算好等待时间,进行等待
  6. finally释放第2步的分布式锁
  7. 将得到的信号量放入不可变List中
  8. 放回CONTINUE

这里有几个地方需要说明一下

  1. 加锁的问题
    • 内部使用的是InterProcessMutex,Shared Reentrant Lock介绍过,这个锁是可重入的
      • 即持有锁的线程是可以直接再次进入
    • 所以在上述第5步时,在已经获得了分布式锁的情况下,仍然需要一个synchronized同步锁来控制本地多线程的并发情况
  2. zk节点
    • 用于分布式锁的节点:path + /locks
      • 锁的临时有序节点
      • 这个节点,会随着信号量申请动作的完成(不论是否申请成功)后,在释放锁时被清理掉(删除)
    • 用于信号量的节点:path + /leases
      • 信号量的临时有序节点
      • 这个节点path会被封装到Lease对象中
        • 随着对信号量使用完成,而执行close方法
          • 这个方法会清理掉信号量节点(删除)
5.4.2 小结

可以简单归纳一下加锁的过程:

  1. 通过一个最大为1的信号量来控制加锁过程
    1. 通过一个分布式可重入锁,解决进程之间的竞争
    2. 通过一个synchronized同步锁,解决线程之间的竞争
  2. 由于全局只有一个信号量可用,所以不论是进程间,还是线程间,还是同一线程都是只有着一个锁
    • 只要不释放锁,即便是同一个线程也无法再次申请锁

5.5 释放锁

public void release() throws Exception
{
    Lease lease = this.lease;
    Preconditions.checkState(lease != null, "Not acquired");
    this.lease = null;
    lease.close();
}

可以看到Shared Lock的释放逻辑就是关闭信号量。所以,再来看看org.apache.curator.framework.recipes.locks.Lease#close实现逻辑。

上一节介绍过,Lease的实现是在:org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2#makeLease

private Lease makeLease(final String path)
{
    return new Lease()
    {
        @Override
        public void close() throws IOException
        {
            try
            {
                client.delete().guaranteed().forPath(path);
            }
            catch ( KeeperException.NoNodeException e )
            {
                log.warn("Lease already released", e);
            }
            catch ( Exception e )
            {
                ThreadUtils.checkInterrupted(e);
                throw new IOException(e);
            }
        }

        @Override
        public byte[] getData() throws Exception
        {
            return client.getData().forPath(path);
        }

        @Override
        public String getNodeName() {
            return ZKPaths.getNodeFromPath(path);
        }
    };
}

可以看到close方法,实际就是删除信号量节点,从而达到释放信号量的作用。

6. 测试

由于这个与Shared Reentrant Lock类似,所以,这个例子就看看在单一线程中重入的情况。

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.InterProcessSemaphoreMutex
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/30.
 */
class InterProcessSemaphoreMutexTest {

    val LATCH_PATH: String = "/test/locks/ipsm"

    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: InterProcessSemaphoreMutex = InterProcessSemaphoreMutex(client, LATCH_PATH)

        while (true) {

            if (lock.acquire(3, TimeUnit.SECONDS)) {
                println("$id 加锁成功 $time")
                while (lock.isAcquiredInThisProcess) {
                    println("$id 执行 $time")
                    TimeUnit.SECONDS.sleep(2)
                    if (Math.random() > 0.5) {
                        if (lock.acquire(3, TimeUnit.SECONDS)) {
                            println("$id 再次加锁成功 $time")
                        } else {
                            println("$id 再次加锁失败 $time")
                        }
                    }
                    if (Math.random() > 0.5) {
                        println("$id 释放锁 $time")
                        lock.release()
                    }
                }
            } else {
                println("$id 加锁失败 $time")
            }
        }
        println("$id 结束: $time")

    }
}

运行情况:

id : xPZcpRyivX 
xPZcpRyivX 加锁成功 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 释放锁 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 加锁成功 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 释放锁 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 加锁成功 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 再次加锁失败 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 释放锁 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 加锁成功 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 再次加锁失败 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 释放锁 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 加锁成功 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 执行 Tue May 30 16:02:10 CST 2017
xPZcpRyivX 再次加锁失败 Tue May 30 16:02:10 CST 2017

可以看到,同一个线程中,再次加锁都是失败,没有成功的

zookeeper节点:

ls /test/locks/ipsm
[leases, locks]

ls /test/locks/ipsm/locks
[_c_cad2ad46-127d-4871-a6f8-7c11c0175f9a-lock-0000000014]

get /test/locks/ipsm/locks/_c_1b35ce47-ff75-46bc-8aea-483117fbf803-lock-0000000020
192.168.60.165
cZxid = 0x1e21a
ctime = Tue May 30 16:03:28 CST 2017
mZxid = 0x1e21a
mtime = Tue May 30 16:03:28 CST 2017
pZxid = 0x1e21a
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x15156529fae07fa
dataLength = 14
numChildren = 0

ls /test/locks/ipsm/leases
[_c_a3198a00-da20-4d43-8b1e-76c2101ce5ef-lease-0000000043, _c_0dc5f8e5-dc77-4a83-a782-6184d584a014-lease-0000000041]

get /test/locks/ipsm/leases/_c_a774be87-1867-4041-9ad7-ef14dcdfa315-lease-0000000049
192.168.60.165
cZxid = 0x1e290
ctime = Tue May 30 16:05:22 CST 2017
mZxid = 0x1e290
mtime = Tue May 30 16:05:22 CST 2017
pZxid = 0x1e290
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x15156529fae07fa
dataLength = 14
numChildren = 0

可以看到

  • /test/locks/ipsm下会有两个节点,一个是锁的,一个是信号量的
  • 都是临时有序节点
  • 偶尔会看到有两个信号量节点
    • 这并不是说有两个锁产生了
    • 根据上文对加锁过程的分析,是先创建信号量节点,然后再查询出来列表,进行数量上的判断
    • 所以,偶尔的出现两个信号量节点,并不是分配了两个,而是说再那个时间点,出现了锁竞争

转载于:https://my.oschina.net/roccn/blog/911335

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值