分布式锁-curator 原理剖析

1,655 阅读6分钟

一、代码演示

1.1 依赖

<dependencies>
  <dependencie>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.1</version>
  </dependencie>
</dependencies>
GroupID/OrgArtifactID/NameDescription
org.apache.curatorcurator-recipesAll of the recipes. Note: this artifact has dependencies on client and framework and, so, Maven (or whatever tool you're using) should pull those in automatically.

因为 curator-recipes 这个包含了 client 和 framework ,所以我们只需要引入这个依赖即可。

1.2 代码使用

public static void main(String[] agrs) {
    // 创建链接
    RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3)
    CuratorFramework client = CuratorFrameworkFactory.newClient(zookeeperConnectionString, retryPolicy);
    client.start();   
    
    // 创建节点
    client.create()
        .creatingParentsIfNeeded() // 如果父目录不存在,把父目录创建出来
        .forPath("/my/path", "value".getBytes());
    
    // 尝试获取锁
    InterProcessMutex lock = new InterProcessMutex(client, lockPath);
    if ( lock.acquire(maxWait, waitUnit) ) 
    {
        try 
        {
            // do some work inside of the critical section here
        }
        finally
        {
            lock.release();
        }
    }
    
    // 选举
    LeaderSelectorListener listener = new LeaderSelectorListenerAdapter()
    {
        public void takeLeadership(CuratorFramework client) throws Exception
        {
            // this callback will get called when you are the leader
            // do whatever leader work you need to and only exit
            // this method when you want to relinquish leadership
        }
    }

    LeaderSelector selector = new LeaderSelector(client, path, listener);
    selector.autoRequeue();  // not required, but this is behavior that you will probably expect
    selector.start();
    
}

二、源码剖析

2.1 可重入锁

public void curatorTest() throws Exception {
        // 创建链接
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("ip:host", retryPolicy);
        client.start();
        // 创建锁对象
        InterProcessMutex mutex = new InterProcessMutex(client, "/lock/mylock");
        // 加锁
        mutex.acquire();
        Thread.sleep(3000);
        // 释放锁
        mutex.release();
    }

这个加锁的主要流程就是通过 acquire 方法进行获取锁,内部是在创建 临时顺序节点 ,然后从 zk 中获取到当前父目录下的所有子节点,并对所有的子节点进行排序,将这个创建出来的临时顺序节点和排序后的所有的子节点的集合进行对比,看看本次创建出来的节点是否处于集合中的首位,如果处于首位的话,就代表该客户端加锁成功,并将信息,放入到 ConcurrentHashMap 中,这里是每个线程对应一个锁信息,这样做主要就是为了实现可重入,当获取到锁的客户端再次加锁的时候,就仅仅是递增 LockData 中的一个属性值。

如果当前创建的节点不处于集合的首位的话,就代表获取锁信息失败,这时会调用 wait 方法,将自己进行阻塞,并计算出来自己当前节点的上一个节点,加上一个 Watch 监听器,当上一个节点释放锁时,监听器就会来回调唤醒方法,主要就是通过 notifyAll 来唤醒所有线程,当线程被唤醒之后,就在走一遍上面的加锁流程。

由于这个锁是可重入锁,所以在释放锁的时候,就是对 LockData 中的数量变量进行递减,判断如果大于0就直接返回,如果等于0的话,就会直接走删除的方法,删除掉这个临时顺序节点,并将自己的信息从 ConcurrentHashMap 移除。

这里通过 临时顺序节点 以及加锁流程,我们可以得出,这个锁是 公平锁 ,先创建节点的客户端,先获取到锁。

curator 可重入锁.png

未命名文件.png

2.2 Semaphore

public void curatorTest() throws Exception {
        // 创建链接
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("ip:host", retryPolicy);
        client.start();
        // 创建 Semaphore
        InterProcessSemaphoreV2 v2 = new InterProcessSemaphoreV2(client,"/lock/semaphor",3);
        Lease lease = v2.acquire();
        v2.returnLease(lease);
    }

Semaphore 主要就是用来实现多个客户端同时获得锁的一个场景,基本流程的话,首先会通过我们设置的 /lock/semaphor 路径,在该路径下会在创建一个 /lock 的子路径,从子路径中,创建 临时顺序节点 作为第一个锁对象,其加锁原理和上面分析的可重入锁一致,当客户端获取到 /lock 下的锁后,会创建一个和 /lock 同级的 /lease 路径,在这个路径下创建 临时顺序节点,代表 Semaphore ,在上面的代码中,设置的是 最多允许三个客户端获取到 Semaphore ,所以,当客户端在 /lease 路径下创建完节点之后,会获取到 /lease 节点下的全部的节点信息,判断其个数是否 <= 我们设置的个数 ,如果小于等于我们设置的个数,就代表还没有超标,这时候,会构建一个 Lease 对象,这个对象重写了 close(), getData() 方法,最后将 /lock 路径下的锁进行释放,返回新建好的 Lease 对象,如果 /lease 节点下的节点个数大于了我们设置的个数,那么此时就会调用 Object.wait() 进行阻塞,等待别的客户端释放锁之后,再次进行判断,加锁。

在关闭的时候,就会用到在加锁时创建好的 Lease 对象,调用它重写好的 Close() 方法,删除掉当前的临时顺序节点,由 Watch 监听器唤醒所以被阻塞的对象。

Curator Semaphore.png

2.3 非可重入锁

public void curatorTest() throws Exception {
        // 创建链接
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("ip:host", retryPolicy);
        client.start();
        // 创建锁对象
        InterProcessSemaphoreMutex v3 = new InterProcessSemaphoreMutex(client,"/lock/share");
        v3.acquire();
        v3.release();

    }

这个的实现很简单,就是包装了一下 Semaphore ,就是将允许的客户端设置成了一个,仅此而已,剩下的所有流程和 Semaphore 都是一样的。

2.4 可重入读写锁

    public void curatorTest() throws Exception {
        // 创建链接
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("ip:host", retryPolicy);
        client.start();
        // 创建锁对象
        InterProcessReadWriteLock v4 = new InterProcessReadWriteLock(client,"/lock/readWrite");
        InterProcessMutex wLock = v4.writeLock();
        InterProcessMutex rLock = v4.readLock();
        
        wLock.acquire();
        wLock.release();

        rLock.acquire();
        rLock.release();
    }
读锁 + 读锁

在这加读锁的逻辑非常简单,就是在路径下创建一个 临时顺序节点,这个节点名称会拼接上 Read 字样,然后判断这个节点的索引位置是小于 Integer.MAX_VALUE 的就可以,这就是相当于,直接允许多个客户端直接加读锁,没有什么限制。

至于读锁释放,就是直接删除掉节点,很简单。

读锁 + 写锁

在这里已经加上了一个读写,也就是说在路径下已经有了一个节点,那么在加写锁的时候,写锁会直接创建出来一个带有 WRITE 字样的临时顺序节点,然后获取到当前路径下的全部节点的集合,判断创建好的节点是否处于第一位,如果不是处于第一位的话,就会给自己的前一个节点加上 Watch 监听,然后调用 wait() 方法,将自己进行堵塞,也就是说,读锁 + 写锁,是会堵塞住,只有在写锁处于首位的时候,才能加成功。

写锁 + 读锁
if ( writeMutex.isOwnedByCurrentThread() )
        {
            return new PredicateResults(null, true);
        }

        int         index = 0;
        int         firstWriteIndex = Integer.MAX_VALUE;
        int         ourIndex = -1;
        for ( String node : children )
        {
            if ( node.contains(WRITE_LOCK_NAME) )
            {
                firstWriteIndex = Math.min(index, firstWriteIndex);
            }
            else if ( node.startsWith(sequenceNodeName) )
            {
                ourIndex = index;
                break;
            }

            ++index;
        }

        boolean     getsTheLock = (ourIndex < firstWriteIndex);

这里借助代码看一下,这个场景是先有了写锁,然后读锁创建了一个临时顺序节点,那么此时,这个路径下就有了两个节点信息。

首先,对写锁进行了判断,看看这个写锁是不是当前加读锁的这个线程加的,如果是的话,就直接返回加锁成功,如果不是的话,就会对这两个节点进行遍历,走到第一个 if ,这里肯定是有一个写锁存在的,那么此时 firstWriteIndex 字段就变为了 0 ,那么在进行第二次循环的时候, ++index ,index 变为了 1 ,ourIndex = index = 1 , 那么在最后判断的时候 1 < 0 是不存在的。

也就是说,当有非当前客户端加了写锁之后,在加入读锁是不被允许的。

写锁 + 写锁

这个根据上面就可得,写锁 + 写锁是失败的,因为第二个要加写锁的创建出来的节点,并不是首位,那么加锁就肯定是失败的。