1. 程式人生 > 實用技巧 >docker-本地部署gitlab

docker-本地部署gitlab

關於Zookeeper的介紹可以看這篇文章:Zookeeper學習筆記

ZNode節點種類

  • 臨時節點 -客戶端與zookeeper斷開連線後,該節點會自動刪除

  • 臨時有序節點 - 客戶端與zookeeper斷開連線後,該節點會自動刪除,但是這些節點都是有序排列的。

  • 持久節點 -客戶端與zookeeper斷開連線後,該節點依然存在

  • 持久節點 -客戶端與zookeeper斷開連線後,該節點依然存在,但是這些節點都是有序排列的。

錯誤的實現分散式鎖方式

鎖原理

多個客戶端同時去建立同一個臨時節點,哪個客戶端第一個建立成功,就成功的獲取鎖,其他客戶端獲取失敗。

獲取鎖的流程

這裡我們使用的是臨時節點

  • 四個客戶端同時建立一個臨時節點。

  • 誰第一個建立成功臨時節點,就代表持有了這個鎖(這裡臨時節點就代表鎖)。

  • 其他紅色的客戶端判斷已經有人建立成功了,就開始監聽這個臨時節點的變化。

釋放鎖的流程

  • 紅色線的客戶端執行任務完畢,與zookeeper斷開了連線。

  • 這時候臨時節點會自動被刪除掉,因為他是臨時的。

  • 其他綠色線的客戶端watch監聽到臨時節點刪除了,就會一擁而上去建立臨時節點(也就是建立鎖)

存在的問題分析

當臨時節點被刪除的時候,其餘3個客戶端一擁而上搶著建立節點。3個節點比較少,效能上看不出什麼問題。

那如果是一千個客戶端在監聽節點呢?一旦節點被刪除了,會喚醒一千個客戶端,一千個客戶端同時來建立節點。但是隻有一個客戶端能建立成功,卻要讓一千個客戶端來競爭。對zookeeper的壓力會很大,同時浪費這些客戶端的執行緒資源,其中有999個客戶端是白跑一趟的。

這就叫做驚群現象,也叫羊群現象。

一個節點釋放刪除了,卻要驚動一千個客戶端,這種做法太傻了吧。

正確的實現分散式鎖方式

這裡用的是順序臨時節點

鎖原理

多個客戶端來競爭鎖,各自建立自己的節點,按照順序建立,誰排在第一個,誰就成功的獲取了鎖。

就像排隊買東西一樣,誰排在第一個,誰就先買。

建立鎖的過程

  • A、B、C、D 四個客戶端來搶鎖

  • A先來了,他建立了000001的臨時順序節點,他發現自己是最小的節點,那麼就成功的獲取到了鎖

  • 然後B來獲取鎖,他按照順序建立了000001的臨時順序節點,發現前面有一個比他小的節點,那麼就獲取鎖失敗。他開始監聽A客戶端,看他什麼時候能釋放鎖

  • 同理C和D

釋放鎖的過程

  • A客戶端執行完任務後,斷開了和zookeeper的會話,這時候臨時順序節點自動刪除了,也就釋放了鎖

  • B客戶端一直在虎視眈眈的watch監聽著A,發現他釋放了鎖,立馬就判斷自己是不是最小的節點,如果是就獲取鎖成功

  • C監聽著B,D監聽著C

合理性分析

A釋放鎖會喚醒B,B獲取到鎖,對C和D是沒有影響的,因為B的節點並沒有發生變化。

同時B釋放鎖,喚醒C,C獲取鎖,對D是沒有影響的,因為C的節點沒有變化。

同理D。。。。

釋放鎖的操作,只會喚醒下一個客戶端,不會喚醒所有的客戶端。所以這種方案不存在驚群現象

ps:建立臨時節點 = 建立鎖,刪除臨時節點 = 釋放鎖。

程式碼測試

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.1.0</version>
        </dependency>
@Slf4j
public class CuratorTest {

    private static final String ZK_IP_LIST = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
    private static final String ZK_LOCK_PATH = "/demo/distributedLockTest";
    private static CuratorFramework curatorClient = null;

    /**
     * 初始化zk連線
     */
    @BeforeAll
    public static void init() {
        //建立重試策略
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        curatorClient = CuratorFrameworkFactory.newClient(ZK_IP_LIST, retryPolicy);
        curatorClient.start();
    }

    /**
     * 分散式鎖測試
     */
    @Test
    public void distributedLockTest() {
        log.info("===distributedLockTest====start===============");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                tryLockTest();
            }).start();
        }

        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("===distributedLockTest====end===============");
    }

    private void tryLockTest() {
        String threadName = Thread.currentThread().getName();
        log.info("===Thread=={}===start===", threadName);
        InterProcessMutex lock = new InterProcessMutex(curatorClient, ZK_LOCK_PATH);

        // 嘗試加鎖,最多等待10秒,上鎖以後30秒自動解鎖
        boolean lockFlag = false;
        try {
            // 嘗試去獲取鎖,10秒沒獲取到鎖,則返回false
            lockFlag = lock.acquire(10, TimeUnit.SECONDS);
            if (!lockFlag) {
                log.info("===Thread=={}==lockFlag={}==沒有獲取到鎖,退出===", threadName, lockFlag);
                return;
            }

            log.info("===Thread=={}============getLock===", threadName);
            // 模擬業務邏輯
            Thread.sleep(2000);
        } catch (Exception e) {
            log.error("執行異常,e:{}", ExceptionUtils.getStackTrace(e));
        } finally {
            log.info("===Thread=={}==========isOwnedByCurrentThread={}", threadName, lock.isOwnedByCurrentThread());
            // 當前執行緒是否持有所的判斷
            if (lock.isOwnedByCurrentThread()) {
                try {
                    lock.release();
                } catch (Exception e) {
                    log.info("===Thread=={}========鎖釋放異常===e:{}", threadName, ExceptionUtils.getStackTrace(e));
                }
            }
        }
        log.info("===Thread=={}==lockFlag={}=end===", threadName, lockFlag);
    }

}

測試結果:

同時觀察/demo/distributedLockTest節點下出現了9個臨時順序節點:

程式結束後,我們在重新整理zookeeper客戶端,發現/demo/distributedLockTest目錄下的臨時順序節點已經被自動刪除了。

總結

為什麼不採用持久節點呢?

因為持久節點必須要客戶端手動刪除,否則他會一直存在zookeeper中。如果我們的客戶端獲取到了鎖,還沒釋放鎖就突然宕機了,那麼這個鎖會一直存在不被釋放。導致其他客戶端無法獲取鎖。

zookeeper實現的鎖功能是比較健全的,但是效能上稍微差一些。比如zookeeper要維護叢集自身資訊的一致性,頻繁建立和刪除節點等原因。

如果僅僅是為了實現分散式鎖而維護一套zookeeper叢集,有點浪費了。如果公司本來就有zookeeper叢集,同時併發不是非常大的情況下,可以考慮zookeeper實現分散式鎖。

Redis在分散式鎖方面的效能要高於zookeeper。但是reis分散式鎖存在節點宕機的問題,可能導致重複獲取鎖。


Redission分散式鎖可以見:Redisson分散式鎖以及其底層原理

參考:

zookeeper分散式鎖[視訊]

zookeeper實現分散式鎖