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分散式鎖以及其底層原理
參考: