博客 / 詳情

返回

【Zookeeper源碼閲讀】六、分佈式鎖的實現原理

六、分佈式鎖的實現原理

實現分佈式鎖的方式有許多種,比如使用數據庫鎖、redis等等。而Zookeeper也可以用於實現分佈式鎖,下面通過源碼來介紹Zookeeper是如何來實現分佈式鎖的?


6.1 驚羣效應

假設用節點"/lock"來表示這個分佈式鎖,我們第一時間可能會想到通過判斷節點"/lock"存在與否,如果不存在則創建該節點,表示獲取到這個鎖,否則監聽該節點,等待鎖的釋放。如下圖所示
image.png

這種方案在客户端數據較多的時候,所有客户端都監聽該"/lock"節點,而當"/lock"節點移除時,zk服務需要做大量的通知處理,導致zk服務的性能驟降。這種現象就叫驚羣效應(網上很多地方翻譯成羊羣效應,個人感覺驚羣效應更貼切一些)


6.2 實現原理

在Zookeeper的源碼中已經有分佈式鎖的實現,在zookeeper-recipes/zookeeper-recipes-lock目錄下。在節點"/lock"下的子節點都是EPHEMERAL_SEQUENTIAL臨時順序節點,第一個子節點為持有當前分佈式鎖的線程,後續的子節點監聽前一個子節點,這樣就解決了驚羣效應的問題

其實現的示意圖如下:
image.png

大致步驟可以分為以下幾步:

  • 判斷/lock下面是否有子節點
  • 如果沒有,説明當前分佈式鎖未被持有,則創建臨時順序子節點,節點名稱為x-sessionId-序列號
  • 如果有,説明當前分佈式鎖已被持有,在該目錄下創建臨時順序節點,並監聽它的上一個子節點
  • 當第一個子節點釋放鎖,第二個節點監聽到後,重新調用lock()方法判斷當前是不是拿到了鎖。如此反覆

6.3 源碼解讀

調用lock()方法來獲取分佈式鎖,源碼如下

public synchronized boolean lock() throws KeeperException, InterruptedException {
        if (isClosed()) {
            return false;
        }
        // 確保路徑存在,不存在則創建
        ensurePathExists(dir);
        
        // 嘗試獲取鎖
        return (Boolean) retryOperation(zop);
    }

底層調用LockZooKeeperOperation類的execute()方法,嘗試獲取鎖,如果拿不到,則監聽上一個子節點,代碼如下

public boolean execute() throws KeeperException, InterruptedException {
    do {
        if (id == null) {
            long sessionId = zookeeper.getSessionId();
            String prefix = "x-" + sessionId + "-";
            // 判斷該會話是否之前已經有創建子節點,如果有,則拿之前創建好的id名,否則新建子節點
            findPrefixInChildren(prefix, zookeeper, dir);
            idName = new ZNodeName(id);
        }
        List<String> names = zookeeper.getChildren(dir, false);
        if (names.isEmpty()) {
            LOG.warn("No children in: {} when we've just created one! Lets recreate it...", dir);
            // 重建節點
            id = null;
        } else {
            // 對這些子節點id進行排序,並拿到比當前子節點id小的列表
            SortedSet<ZNodeName> sortedNames = new TreeSet<>();
            for (String name : names) {
                sortedNames.add(new ZNodeName(dir + "/" + name));
            }
            ownerId = sortedNames.first().getName();
            SortedSet<ZNodeName> lessThanMe = sortedNames.headSet(idName);

            // 如果比當前子節點id小的列表不為空,則監聽上一個子節點
            if (!lessThanMe.isEmpty()) {
                ZNodeName lastChildName = lessThanMe.last();
                lastChildId = lastChildName.getName();
                LOG.debug("Watching less than me node: {}", lastChildId);
                Stat stat = zookeeper.exists(lastChildId, new LockWatcher());
                if (stat != null) {
                    return Boolean.FALSE;
                } else {
                    LOG.warn("Could not find the stats for less than me: {}", lastChildName.getName());
                }
            } else {
                // 否則判斷當前子節點id是不是最小id,如果是,則返回true,表示獲取到鎖
                if (isOwner()) {
                    LockListener lockListener = getLockListener();
                    if (lockListener != null) {
                        lockListener.lockAcquired();
                    }
                    return Boolean.TRUE;
                }
            }
        }
    }
    while (id == null);
    return Boolean.FALSE;
}

當客户端釋放鎖時,調用unlock()方法,刪除當前對應id的子節點

public synchronized void unlock() throws RuntimeException {
    if (!isClosed() && id != null) {
        try {
            ZooKeeperOperation zopdel = () -> {
                // 刪除當前對應id的子節點
                zookeeper.delete(id, -1);
                return Boolean.TRUE;
            };
            zopdel.execute();
        } catch (InterruptedException e) {
            // 略
        } finally {
            LockListener lockListener = getLockListener();
            if (lockListener != null) {
                lockListener.lockReleased();
            }
            id = null;
        }
    }
}
user avatar u_15082365 頭像 songofhawk 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.