基於ZooKeeper Curator實現分佈式鎖

基於ZooKeeper分佈式鎖的流程

1. 客戶端連接上zookeeper,並在指定節點(locks)下創建臨時順序節點node_n

2. 客戶端獲取locks目錄下所有children節點

3. 客戶端對子節點按節點自增序號從小到大排序,並判斷自己創建的節點是不是序號最小的,若是則獲取鎖;若不是,則監聽比該節點小的那個節點的刪除事件

4. 獲得子節點變更通知後重復此步驟直至獲得鎖;

5. 執行業務代碼,完成業務流程後,刪除對應的子節點釋放鎖。

Q&A

步驟1中爲什麼創建臨時節點?

zk臨時節點的特性是,當客戶端與zk服務器的連接中斷時,客戶端創建的臨時節點將自動刪除;所以創建臨時節點是爲了保證在發生故障的情況下鎖也能被釋放,比如場景1:假如客戶端a獲得鎖之後客戶端所在機器宕機了,客戶端沒有主動刪除子節點;如果創建的是永久的節點,那麼這個鎖永遠不會釋放,導致死鎖;而如果創建的是臨時節點,客戶端宕機後,心跳檢測時zookeeper沒有收到客戶端的心跳包就會判斷該會話已失效,並且將臨時節點刪除從而釋放鎖。

步驟3中爲什麼不是監聽locks目錄,而僅監聽比自己小的那一個節點?

如果每個客戶端都監聽locks目錄,那麼當某個客戶端釋放鎖刪除子節點時,其他所有的客戶端都會收到監聽事件,產生羊羣效應,並且zookeeper在通知所有客戶端時會阻塞其他的操作,最好的情況應該只喚醒新的最小節點對應的客戶端。應該怎麼做呢?在設置事件監聽時,每個客戶端應該對剛好在它之前的子節點設置事件監聽,例如子節點列表爲/lock/lock-0000000000、/lock/lock-0000000001、/lock/lock-0000000002,序號爲1的客戶端監聽序號爲0的子節點刪除消息,序號爲2的監聽序號爲1的子節點刪除消息。

兩種實現方式

zk分佈式鎖的實現有兩種方式,一是原生方式,使用java和zk api書寫以上邏輯實現分佈式鎖,操作zookeeper使用的是apache提供的zookeeper的包。通過實現Watch接口,實現process(WatchedEvent event)方法來實施監控,使CountDownLatch來完成監控,在等待鎖的時候使用CountDownLatch來計數,等到後進行countDown,停止等待,繼續運行。

另一種方式是使用Curator框架來實現分佈式鎖,Curator是Netflix公司一個開源的zookeeper客戶端,在原生API接口上進行了包裝,解決了很多ZooKeeper客戶端非常底層的細節開發。同時內部實現了諸如Session超時重連,Watcher反覆註冊等功能,實現了Fluent風格的API接口,是使用最廣泛的zookeeper客戶端之一。

兩種方式對比來說,原生方式自己實現邏輯比較靈活,個性化高但是開發量比較大,使用Curator實現分佈式鎖非常簡單,幾行代碼就可以搞定,隱藏了很多實現細節。

Curator實現分佈式鎖

/**
 * Created by ErNiu on 2018/1/29.
 */
public class testlock {
    private static final String ZK_ADDRESS = "10.2.1.1:2181";
    private static final String ZK_LOCK_PATH = "/zktest/lock0";

    /**
     * 下面的程序會啓動幾個線程去爭奪鎖,拿到鎖的線程會佔用5秒
     */
    public static void main(String[] args) throws InterruptedException {
        // 1.Connect to zk
        final CuratorFramework client = CuratorFrameworkFactory.newClient(ZK_ADDRESS, new RetryNTimes(10, 5000));
        client.start();

        System.out.println(client.getState());

        System.out.println("zk client start successfully!");

        final InterProcessMutex mutex = new InterProcessMutex(client, ZK_LOCK_PATH);

        for (int i = 0; i < 3; i++) {
            Runnable myRunnable = new Runnable() {
                public void run() {
                    doWithLock(client, mutex);
                }
            };
            Thread thread = new Thread(myRunnable, "Thread-" + i);
            thread.start();
        }

    }

    private static void doWithLock(CuratorFramework client, InterProcessMutex mutex) {
        try {
            String name = Thread.currentThread().getName();
            if (mutex.acquire(1, TimeUnit.SECONDS)) {

                System.out.println(name + " hold lock");

                System.out.println(client.getChildren().forPath(ZK_LOCK_PATH));

                Thread.sleep(5000L);
                System.out.println(name + " release lock");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                mutex.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

可以看到通過Curator實現分佈式鎖,只需要兩行代碼mutex.acquire()和mutex.release(),上面的代碼同時測試了鎖超時時間的作用,啓動三個線程去獲取鎖,線程2獲取到鎖sleep5秒,超時時間設置爲1s,線程0和線程1阻塞等待1s後,便會拋出異常。這裏仍然存在一個問題,假如線程2獲取到鎖,並且線程2因爲自身原因,一直不釋放鎖。這就會導致其他線程無法正常運行,所以需要對獲取到鎖的線程設置一個超時時間,超過規定時間仍未執行完,則強制釋放鎖,並拋出異常,來保證程序不會阻塞。當然這需要對Curator再次進行封裝。
image

小結

本篇文章對分佈式鎖的實現邏輯進行了簡單介紹,並且講述兩種實現zk分佈式鎖的方式,原生方式還是需要手動實現一下的,但是工作中爲了效率還是建議使用Curator。

額,Curator源碼分析下篇進行吧…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章