文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈爲小夥伴奉上如下珍貴的學習資源:html
高併發 必讀 的精彩博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
Zookeeper 分佈式鎖 (圖解+秒懂+史上最全) | Webflux(史上最全) |
SpringCloud gateway (史上最全) | TCP/IP(圖解+秒懂+史上最全) |
10分鐘看懂, Java NIO 底層原理 | Feign原理 (圖解) |
更多精彩博文 ..... | 請參見【 瘋狂創客圈 高併發 總目錄 】 |
在單體的應用開發場景中,涉及併發同步的時候,你們每每採用synchronized或者Lock的方式來解決多線程間的同步問題。但在分佈式集羣工做的開發場景中,那麼就須要一種更加高級的鎖機制,來處理種跨JVM進程之間的數據同步問題,這就是分佈式鎖。java
最經典的分佈式鎖是可重入的公平鎖。什麼是可重入的公平鎖呢?直接講解的概念和原理,會比較抽象難懂,仍是從具體的實例入手吧!這裏用一個簡單的故事來類比,估計就簡單多了。node
故事發生在一個沒有自來水的古代,在一個村子有一口井,水質很是的好,村民們都搶着取井裏的水。井就那麼一口,村裏的人不少,村民爲爭搶取水打架鬥毆,甚至頭破血流。程序員
問題老是要解決,因而村長絞盡腦汁,最終想出了一個憑號取水的方案。井邊安排一個看井人,維護取水的秩序。取水秩序很簡單:面試
(1)取水以前,先取號;算法
(2)號排在前面的,就能夠先取水;sql
(3)先到的排在前面,那些後到的,一個一個挨着,在井邊排成一隊。apache
取水示意圖,如圖10-3所示。
編程
圖10-3 排隊取水示意圖設計模式
這種排隊取水模型,就是一種鎖的模型。排在最前面的號,擁有取水權,就是一種典型的獨佔鎖。另外,先到先得,號排在前面的人先取到水,取水以後就輪到下一個號取水,挺公平的,說明它是一種公平鎖。
什麼是可重入鎖呢?
假定,取水時以家庭爲單位,家庭的某人拿到號,其餘的家庭成員過來打水,這時候不用再取號,如圖10-4所示。
圖10-4 同一家庭的人不須要重複排隊
圖10-4中,排在1號的家庭,老公取號,假設其老婆來了,直接排第一個,正所謂妻憑夫貴。再看上圖的2號,父親正在打水,假設其兒子和女兒也到井邊了,直接排第二個,所謂子憑父貴。總之,若是取水時以家庭爲單位,則同一個家庭,能夠直接複用排號,不用從後面排起從新取號。
以上這個故事模型中,取號一次,能夠用來屢次取水,其原理爲可重入鎖的模型。在重入鎖模型中,一把獨佔鎖,能夠被屢次鎖定,這就叫作可重入鎖。
理解了經典的公平可重入鎖的原理後,再來看在分佈式場景下的公平可重入鎖的原理。經過前面的分析,基本能夠斷定:ZooKeeper
的臨時順序節點,天生就有一副實現分佈式鎖的胚子。爲何呢?
(一) ZooKeeper的每個節點,都是一個自然的順序發號器。
在每個節點下面建立臨時順序節點(EPHEMERAL_SEQUENTIAL)類型,新的子節點後面,會加上一個次序編號,而這個生成的次序編號,是上一個生成的次序編號加一。
例如,有一個用於發號的節點「/test/lock」爲父親節點,能夠在這個父節點下面建立相同前綴的臨時順序子節點,假定相同的前綴爲「/test/lock/seq-」。第一個建立的子節點基本上應該爲/test/lock/seq-0000000000,下一個節點則爲/test/lock/seq-0000000001,依次類推,若是10-5所示。
圖10-5 Zookeeper臨時順序節點的自然的發號器做用
(二) ZooKeeper節點的遞增有序性,能夠確保鎖的公平
一個ZooKeeper分佈式鎖,首先須要建立一個父節點,儘可能是持久節點(PERSISTENT類型),而後每一個要得到鎖的線程,都在這個節點下建立個臨時順序節點。因爲ZK節點,是按照建立的次序,依次遞增的。
爲了確保公平,能夠簡單的規定:編號最小的那個節點,表示得到了鎖。因此,每一個線程在嘗試佔用鎖以前,首先判斷本身是排號是否是當前最小,若是是,則獲取鎖。
(三)ZooKeeper的節點監聽機制,能夠保障佔有鎖的傳遞有序並且高效
每一個線程搶佔鎖以前,先嚐試建立本身的ZNode。一樣,釋放鎖的時候,就須要刪除建立的Znode。建立成功後,若是不是排號最小的節點,就處於等待通知的狀態。等誰的通知呢?不須要其餘人,只須要等前一個Znode
的通知就能夠了。前一個Znode刪除的時候,會觸發Znode事件,當前節點能監聽到刪除事件,就是輪到了本身佔有鎖的時候。第一個通知第二個、第二個通知第三個,擊鼓傳花似的依次向後。
ZooKeeper的節點監聽機制,可以很是完美地實現這種擊鼓傳花似的信息傳遞。具體的方法是,每個等通知的Znode節點,只須要監聽(linsten)或者監視(watch)排號在本身前面那個,並且緊挨在本身前面的那個節點,就能收到其刪除事件了。
只要上一個節點被刪除了,就進行再一次判斷,看看本身是否是序號最小的那個節點,若是是,本身就得到鎖。
另外,ZooKeeper的內部優越的機制,能保證因爲網絡異常或者其餘緣由,集羣中佔用鎖的客戶端失聯時,鎖可以被有效釋放。一旦佔用Znode鎖的客戶端與ZooKeeper集羣服務器失去聯繫,這個臨時Znode也將自動刪除。排在它後面的那個節點,也能收到刪除事件,從而得到鎖。正是因爲這個緣由,在建立取號節點的時候,儘可能建立臨時znode
節點,
(四)ZooKeeper的節點監聽機制,能避免羊羣效應
ZooKeeper這種首尾相接,後面監聽前面的方式,能夠避免羊羣效應。所謂羊羣效應就是一個節點掛掉,全部節點都去監聽,而後作出反應,這樣會給服務器帶來巨大壓力,因此有了臨時順序節點,當一個節點掛掉,只有它後面的那一個節點才作出反應。
接下來咱們一塊兒來看看,多客戶端獲取及釋放zk分佈式鎖的整個流程及背後的原理。
首先你們看看下面的圖,若是如今有兩個客戶端一塊兒要爭搶zk上的一把分佈式鎖,會是個什麼場景?
若是你們對zk還不太瞭解的話,建議先自行百度一下,簡單瞭解點基本概念,好比zk有哪些節點類型等等。
參見上圖。zk裏有一把鎖,這個鎖就是zk上的一個節點。而後呢,兩個客戶端都要來獲取這個鎖,具體是怎麼來獲取呢?
我們就假設客戶端A搶先一步,對zk發起了加分佈式鎖的請求,這個加鎖請求是用到了zk中的一個特殊的概念,叫作「臨時順序節點」。
簡單來講,就是直接在"my_lock"這個鎖節點下,建立一個順序節點,這個順序節點有zk內部自行維護的一個節點序號。
好比說,第一個客戶端來搞一個順序節點,zk內部會給起個名字叫作:xxx-000001。而後第二個客戶端來搞一個順序節點,zk可能會起個名字叫作:xxx-000002。你們注意一下,最後一個數字都是依次遞增的,從1開始逐次遞增。zk會維護這個順序。
因此這個時候,假如說客戶端A先發起請求,就會搞出來一個順序節點,你們看下面的圖,Curator框架大概會弄成以下的樣子:
你們看,客戶端A發起一個加鎖請求,先會在你要加鎖的node下搞一個臨時順序節點,這一大坨長長的名字都是Curator框架本身生成出來的。
而後,那個最後一個數字是"1"。你們注意一下,由於客戶端A是第一個發起請求的,因此給他搞出來的順序節點的序號是"1"。
接着客戶端A建立完一個順序節點。還沒完,他會查一下"my_lock"這個鎖節點下的全部子節點,而且這些子節點是按照序號排序的,這個時候他大概會拿到這麼一個集合:
接着客戶端A會走一個關鍵性的判斷,就是說:唉!兄弟,這個集合裏,我建立的那個順序節點,是否是排在第一個啊?
若是是的話,那我就能夠加鎖了啊!由於明明我就是第一個來建立順序節點的人,因此我就是第一個嘗試加分佈式鎖的人啊!
bingo!加鎖成功!你們看下面的圖,再來直觀的感覺一下整個過程。
接着假如說,客戶端A都加完鎖了,客戶端B過來想要加鎖了,這個時候他會幹同樣的事兒:先是在"my_lock"這個鎖節點下建立一個臨時順序節點,此時名字會變成相似於:
你們看看下面的圖:
客戶端B由於是第二個來建立順序節點的,因此zk內部會維護序號爲"2"。
接着客戶端B會走加鎖判斷邏輯,查詢"my_lock"鎖節點下的全部子節點,按序號順序排列,此時他看到的相似於:
同時檢查本身建立的順序節點,是否是集合中的第一個?
明顯不是啊,此時第一個是客戶端A建立的那個順序節點,序號爲"01"的那個。因此加鎖失敗!
加鎖失敗了之後,客戶端B就會經過ZK的API對他的順序節點的上一個順序節點加一個監聽器。zk自然就能夠實現對某個節點的監聽。
若是你們還不知道zk的基本用法,能夠百度查閱,很是的簡單。客戶端B的順序節點是:
他的上一個順序節點,不就是下面這個嗎?
即客戶端A建立的那個順序節點!
因此,客戶端B會對:
這個節點加一個監聽器,監聽這個節點是否被刪除等變化!你們看下面的圖。
接着,客戶端A加鎖以後,可能處理了一些代碼邏輯,而後就會釋放鎖。那麼,釋放鎖是個什麼過程呢?
其實很簡單,就是把本身在zk裏建立的那個順序節點,也就是:
這個節點給刪除。
刪除了那個節點以後,zk會負責通知監聽這個節點的監聽器,也就是客戶端B以前加的那個監聽器,說:兄弟,你監聽的那個節點被刪除了,有人釋放了鎖。
此時客戶端B的監聽器感知到了上一個順序節點被刪除,也就是排在他以前的某個客戶端釋放了鎖。
此時,就會通知客戶端B從新嘗試去獲取鎖,也就是獲取"my_lock"節點下的子節點集合,此時爲:
集合裏此時只有客戶端B建立的惟一的一個順序節點了!
而後呢,客戶端B判斷本身竟然是集合中的第一個順序節點,bingo!能夠加鎖了!直接完成加鎖,運行後續的業務代碼便可,運行完了以後再次釋放鎖。
接下來就是基於ZooKeeper,實現一下分佈式鎖。首先,定義了一個鎖的接口Lock,很簡單,僅僅兩個抽象方法:一個加鎖方法,一個解鎖方法。Lock接口的代碼以下:
package com.crazymakercircle.zk.distributedLock; /** * create by 尼恩 @ 瘋狂創客圈 **/ public interface Lock { /** * 加鎖方法 * * @return 是否成功加鎖 */ boolean lock() throws Exception; /** * 解鎖方法 * * @return 是否成功解鎖 */ boolean unlock(); }
使用ZooKeeper實現分佈式鎖的算法,有如下幾個要點:
(1)一把分佈式鎖一般使用一個Znode節點表示;若是鎖對應的Znode節點不存在,首先建立Znode節點。這裏假設爲「/test/lock」,表明了一把須要建立的分佈式鎖。
(2)搶佔鎖的全部客戶端,使用鎖的Znode節點的子節點列表來表示;若是某個客戶端須要佔用鎖,則在「/test/lock」下建立一個臨時有序的子節點。
這裏,全部臨時有序子節點,儘可能共用一個有意義的子節點前綴。
好比,若是子節點的前綴爲「/test/lock/seq-」,則第一次搶鎖對應的子節點爲「/test/lock/seq-000000000」,第二次搶鎖對應的子節點爲「/test/lock/seq-000000001」,以此類推。
再好比,若是子節點前綴爲「/test/lock/」,則第一次搶鎖對應的子節點爲「/test/lock/000000000」,第二次搶鎖對應的子節點爲「/test/lock/000000001」,以此類推,也很是直觀。
(3)若是斷定客戶端是否佔有鎖呢?
很簡單,客戶端建立子節點後,須要進行判斷:本身建立的子節點,是否爲當前子節點列表中序號最小的子節點。若是是,則認爲加鎖成功;若是不是,則監聽前一個Znode子節點變動消息,等待前一個節點釋放鎖。
(4)一旦隊列中的後面的節點,得到前一個子節點變動通知,則開始進行判斷,判斷本身是否爲當前子節點列表中序號最小的子節點,若是是,則認爲加鎖成功;若是不是,則持續監聽,一直到得到鎖。
(5)獲取鎖後,開始處理業務流程。完成業務流程後,刪除本身的對應的子節點,完成釋放鎖的工做,以方面後繼節點能捕獲到節點變動通知,得到分佈式鎖。
Lock接口中加鎖的方法是lock()。lock()方法的大體流程是:首先嚐試着去加鎖,若是加鎖失敗就去等待,而後再重複。
lock()方法加鎖的實現代碼,大體以下:
package com.crazymakercircle.zk.distributedLock; import com.crazymakercircle.zk.ZKclient; import lombok.extern.slf4j.Slf4j; import org.apache.curator.framework.CuratorFramework; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * create by 尼恩 @ 瘋狂創客圈 **/ @Slf4j public class ZkLock implements Lock { //ZkLock的節點連接 private static final String ZK_PATH = "/test/lock"; private static final String LOCK_PREFIX = ZK_PATH + "/"; private static final long WAIT_TIME = 1000; //Zk客戶端 CuratorFramework client = null; private String locked_short_path = null; private String locked_path = null; private String prior_path = null; final AtomicInteger lockCount = new AtomicInteger(0); private Thread thread; public ZkLock() { ZKclient.instance.init(); synchronized (ZKclient.instance) { if (!ZKclient.instance.isNodeExist(ZK_PATH)) { ZKclient.instance.createNode(ZK_PATH, null); } } client = ZKclient.instance.getClient(); } @Override public boolean lock() { //可重入,確保同一線程,能夠重複加鎖 synchronized (this) { if (lockCount.get() == 0) { thread = Thread.currentThread(); lockCount.incrementAndGet(); } else { if (!thread.equals(Thread.currentThread())) { return false; } lockCount.incrementAndGet(); return true; } } try { boolean locked = false; //首先嚐試着去加鎖 locked = tryLock(); if (locked) { return true; } //若是加鎖失敗就去等待 while (!locked) { await(); //獲取等待的子節點列表 List<String> waiters = getWaiters(); //判斷,是否加鎖成功 if (checkLocked(waiters)) { locked = true; } } return true; } catch (Exception e) { e.printStackTrace(); unlock(); } return false; } //...省略其餘的方法 }
嘗試加鎖的tryLock方法是關鍵,作了兩件重要的事情:
(1)建立臨時順序節點,而且保存本身的節點路徑
(2)判斷是不是第一個,若是是第一個,則加鎖成功。若是不是,就找到前一個Znode節點,而且保存其路徑到prior_path。
嘗試加鎖的tryLock方法,其實現代碼以下:
/** * 嘗試加鎖 * @return 是否加鎖成功 * @throws Exception 異常 */ private boolean tryLock() throws Exception { //建立臨時Znode locked_path = ZKclient.instance .createEphemeralSeqNode(LOCK_PREFIX); //而後獲取全部節點 List<String> waiters = getWaiters(); if (null == locked_path) { throw new Exception("zk error"); } //取得加鎖的排隊編號 locked_short_path = getShortPath(locked_path); //獲取等待的子節點列表,判斷本身是否第一個 if (checkLocked(waiters)) { return true; } // 判斷本身排第幾個 int index = Collections.binarySearch(waiters, locked_short_path); if (index < 0) { // 網絡抖動,獲取到的子節點列表裏可能已經沒有本身了 throw new Exception("節點沒有找到: " + locked_short_path); } //若是本身沒有得到鎖,則要監聽前一個節點 prior_path = ZK_PATH + "/" + waiters.get(index - 1); return false; } private String getShortPath(String locked_path) { int index = locked_path.lastIndexOf(ZK_PATH + "/"); if (index >= 0) { index += ZK_PATH.length() + 1; return index <= locked_path.length() ? locked_path.substring(index) : ""; } return null; }
建立臨時順序節點後,其完整路徑存放在locked_path成員中;另外還截取了一個後綴路徑,放在
locked_short_path成員中,後綴路徑是一個短路徑,只有完整路徑的最後一層。爲何要單獨保存短路徑呢?
由於,在獲取的遠程子節點列表中的其餘路徑返回結果時,返回的都是短路徑,都只有最後一層路徑。因此爲了方便後續進行比較,也把本身的短路徑保存下來。
建立了本身的臨時節點後,調用checkLocked方法,判斷是不是鎖定成功。若是鎖定成功,則返回true;若是本身沒有得到鎖,則要監聽前一個節點,此時須要找出前一個節點的路徑,並保存在
prior_path
成員中,供後面的await()等待方法去監聽使用。在進入await()等待方法的介紹前,先說下checkLocked
鎖定判斷方法。
在checkLocked()方法中,判斷是否能夠持有鎖。判斷規則很簡單:當前建立的節點,是否在上一步獲取到的子節點列表的第一個位置:
(1)若是是,說明能夠持有鎖,返回true,表示加鎖成功;
(2)若是不是,說明有其餘線程早已先持有了鎖,返回false。
checkLocked()方法的代碼以下:
private boolean checkLocked(List<String> waiters) { //節點按照編號,升序排列 Collections.sort(waiters); // 若是是第一個,表明本身已經得到了鎖 if (locked_short_path.equals(waiters.get(0))) { log.info("成功的獲取分佈式鎖,節點爲{}", locked_short_path); return true; } return false; }
checkLocked方法比較簡單,將參與排隊的全部子節點列表,從小到大根據節點名稱進行排序。排序主要依靠節點的編號,也就是後Znode路徑的10位數字,由於前綴都是同樣的。排序以後,作判斷,若是本身的locked_short_path編號位置排在第一個,若是是,則表明本身已經得到了鎖。若是不是,則會返回false。
若是checkLocked()爲false,外層的調用方法,通常來講會執行await()等待方法,執行奪鎖失敗之後的等待邏輯。
await()也很簡單,就是監聽前一個ZNode節點(prior_path成員)的刪除事件,代碼以下:
private void await() throws Exception { if (null == prior_path) { throw new Exception("prior_path error"); } final CountDownLatch latch = new CountDownLatch(1); //訂閱比本身次小順序節點的刪除事件 Watcher w = new Watcher() { @Override public void process(WatchedEvent watchedEvent) { System.out.println("監聽到的變化 watchedEvent = " + watchedEvent); log.info("[WatchedEvent]節點刪除"); latch.countDown(); } }; client.getData().usingWatcher(w).forPath(prior_path); /* //訂閱比本身次小順序節點的刪除事件 TreeCache treeCache = new TreeCache(client, prior_path); TreeCacheListener l = new TreeCacheListener() { @Override public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception { ChildData data = event.getData(); if (data != null) { switch (event.getType()) { case NODE_REMOVED: log.debug("[TreeCache]節點刪除, path={}, data={}", data.getPath(), data.getData()); latch.countDown(); break; default: break; } } } }; treeCache.getListenable().addListener(l); treeCache.start();*/ latch.await(WAIT_TIME, TimeUnit.SECONDS); }
首先添加一個Watcher監聽,而監聽的節點,正是前面所保存在prior_path成員的前一個節點的路徑。這裏,僅僅去監聽本身前一個節點的變更,而不是其餘節點的變更,提高效率。完成監聽以後,調用latch.await(),線程進入等待狀態,一直到線程被監聽回調代碼中的latch.countDown() 所喚醒,或者等待超時。
說 明
以上代碼用到的CountDownLatch的核心原理和實戰知識,《Netty Zookeeper Redis 高併發實戰》姊妹篇 《Java高併發核心編程(卷2)》。
上面的代碼中,監聽前一個節點的刪除,可使用兩種監聽方式:
(1)Watcher 訂閱;
(2)TreeCache 訂閱。
兩種方式的效果,都差很少。可是這裏的刪除事件,只須要監聽一次便可,不須要反覆監聽,因此使用的是Watcher
一次性訂閱。而TreeCache 訂閱的代碼在源碼工程中已經被註釋,僅僅供你們參考。
一旦前一個節點prior_path節點被刪除,那麼就將線程從等待狀態喚醒,從新一輪的鎖的爭奪,直到獲取鎖,而且完成業務處理。
至此,分佈式Lock加鎖的算法,還差一點就介紹完成。這一點,就是實現鎖的可重入。
什麼是可重入呢?只須要保障同一個線程進入加鎖的代碼,能夠重複加鎖成功便可。
修改前面的lock方法,在前面加上可重入的判斷邏輯。代碼以下:
@Override public boolean lock() { //可重入的判斷 synchronized (this) { if (lockCount.get() == 0) { thread = Thread.currentThread(); lockCount.incrementAndGet(); } else { if (!thread.equals(Thread.currentThread())) { return false; } lockCount.incrementAndGet(); return true; } } //.... }
爲了變成可重入,在代碼中增長了一個加鎖的計數器lockCount
,計算重複加鎖的次數。若是是同一個線程加鎖,只須要增長次數,直接返回,表示加鎖成功。
至此,lock()方法已經介紹完成,接下來,就是去釋放鎖
Lock接口中的unLock()方法,表示釋放鎖,釋放鎖主要有兩個工做:
(1)減小重入鎖的計數,若是最終的值不是0,直接返回,表示成功的釋放了一次;
(2)若是計數器爲0,移除Watchers監聽器,而且刪除建立的Znode臨時節點。
unLock()方法的代碼以下:
/** * 釋放鎖 * * @return 是否成功釋放鎖 */ @Override public boolean unlock() { //只有加鎖的線程,可以解鎖 if (!thread.equals(Thread.currentThread())) { return false; } //減小可重入的計數 int newLockCount = lockCount.decrementAndGet(); //計數不能小於0 if (newLockCount < 0) { throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + locked_path); } //若是計數不爲0,直接返回 if (newLockCount != 0) { return true; } //刪除臨時節點 try { if (ZKclient.instance.isNodeExist(locked_path)) { client.delete().forPath(locked_path); } } catch (Exception e) { e.printStackTrace(); return false; } return true; }
這裏,爲了儘可能保證線程安全,可重入計數器的類型,使用的不是int類型,而是Java併發包中的原子類型——AtomicInteger。
寫一個用例,測試一下ZLock的使用,代碼以下:
@Test public void testLock() throws InterruptedException { for (int i = 0; i < 10; i++) { FutureTaskScheduler.add(() -> { //建立鎖 ZkLock lock = new ZkLock(); lock.lock(); //每條線程,執行10次累加 for (int j = 0; j < 10; j++) { //公共的資源變量累加 count++; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("count = " + count); //釋放鎖 lock.unlock(); }); } Thread.sleep(Integer.MAX_VALUE); }
以上代碼是10個併發任務,每一個任務累加10次,執行以上用例,會發現結果會是預期的和100,若是不使用鎖,結果可能就不是100,由於上面的count是一個普通的變量,不是線程安全的。
說 明
有關線程安全的核心原理和實戰知識,請參閱本書的下一卷《Java高併發核心編程(卷2)》。
原理上一個Zlock實例表明一把鎖,並須要佔用一個Znode永久節點,若是須要不少分佈式鎖,則也須要不少的不一樣的Znode節點。以上代碼,若是要擴展爲多個分佈式鎖的版本,還須要進行簡單改造,這種改造留給各位本身去練習和實現吧。
分佈式鎖Zlock自主實現主要的價值:學習一下分佈式鎖的原理和基礎開發,僅此而已。實際的開發中,若是須要使用到分佈式鎖,並建議去本身造輪子,建議直接使用Curator客戶端中的各類官方實現的分佈式鎖,好比其中的InterProcessMutex
可重入鎖。
這裏提供一個簡單的InterProcessMutex 可重入鎖的使用實例,代碼以下:
@Test public void testzkMutex() throws InterruptedException { CuratorFramework client = ZKclient.instance.getClient(); final InterProcessMutex zkMutex = new InterProcessMutex(client, "/mutex"); ; for (int i = 0; i < 10; i++) { FutureTaskScheduler.add(() -> { try { //獲取互斥鎖 zkMutex.acquire(); for (int j = 0; j < 10; j++) { //公共的資源變量累加 count++; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("count = " + count); //釋放互斥鎖 zkMutex.release(); } catch (Exception e) { e.printStackTrace(); } }); } Thread.sleep(Integer.MAX_VALUE); }
總結一下ZooKeeper分佈式鎖:
(1)優勢:ZooKeeper分佈式鎖(如InterProcessMutex),能有效的解決分佈式問題,不可重入問題,使用起來也較爲簡單。
(2)缺點:ZooKeeper實現的分佈式鎖,性能並不過高。爲啥呢?
由於每次在建立鎖和釋放鎖的過程當中,都要動態建立、銷燬瞬時節點來實現鎖功能。你們知道,ZK中建立和刪除節點只能經過Leader服務器來執行,而後Leader服務器還須要將數據同不到全部的Follower機器上,這樣頻繁的網絡通訊,性能的短板是很是突出的。
總之,在高性能,高併發的場景下,不建議使用ZooKeeper的分佈式鎖。而因爲ZooKeeper的高可用特性,因此在併發量不是過高的場景,推薦使用ZooKeeper的分佈式鎖。
在目前分佈式鎖實現方案中,比較成熟、主流的方案有兩種:
(1)基於Redis的分佈式鎖
(2)基於ZooKeeper的分佈式鎖
兩種鎖,分別適用的場景爲:
(1)基於ZooKeeper的分佈式鎖,適用於高可靠(高可用)而併發量不是太大的場景;
(2)基於Redis的分佈式鎖,適用於併發量很大、性能要求很高的、而可靠性問題能夠經過其餘方案去彌補的場景。
總之,這裏沒有誰好誰壞的問題,而是誰更合適的問題。
最後對本章的內容作個總結:在分佈式系統中,ZooKeeper是一個重要的協調工具。本章介紹了分佈式命名服務、分佈式鎖的原理以及基於ZooKeeper的參考實現。本章的那些實戰案例,建議你們本身去動手掌握,不管是應用實際開始、仍是大公司面試,都是很是有用的。另外,主流的分佈式協調中間件,也不只僅只有Zookeeper,還有很是著名的Etcd中間件。可是從學習的層面來講,兩者之間的功能設計、核心原理都是差很少的,掌握了Zookeeper,Etcd的上手使用也是很容易的。
圖書:《Netty Zookeeper Redis 高併發實戰》 圖書簡介 - 瘋狂創...
圖書:《Netty Zookeeper Redis 高併發實戰》 圖書簡介 - 瘋狂創...