zookeeper 分散式鎖解析

卜寧發表於2020-12-03

一、程式碼演示

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 是不存在的。

也就是說,當有非當前客戶端加了寫鎖之後,在加入讀鎖是不被允許的。

寫鎖 + 寫鎖

這個根據上面就可得,寫鎖 + 寫鎖是失敗的,因為第二個要加寫鎖的建立出來的節點,並不是首位,那麼加鎖就肯定是失敗的。

未命名檔案.png

相關文章