1. 程式人生 > >【分散式鎖的演化】終章!手擼ZK分散式鎖!

【分散式鎖的演化】終章!手擼ZK分散式鎖!

### 前言 這應該是分散式鎖演化的最後一個章節了,相信很多小夥伴們看完這個章節之後在應對高併發的情況下,如何保證執行緒安全心裡肯定也會有譜了。在實際的專案中也可以參考一下老貓的github上的例子,當然程式碼沒有經過特意的封裝,需要小夥伴們自己再好好封裝一下。那麼接下來,就和大家分享一下基於zookeeper的分散式鎖,由於此篇主要分享的是zk的分散式鎖,所以對於zk本身的相關知識點,並不會涉及很多。和分散式鎖實現有關的zk知識點會提及。 ### Zookeeper實現分散式鎖 何為ZK?(為了打字簡單,後續老貓均以ZK來代替zookeeper),相信很多接觸到Dubbo框架的小夥伴可能聽說過ZK,但是具體也沒有詳細地去學習ZK。那麼又如何利用ZK來實現分散式鎖呢?以下我們一個個來看。 #### 什麼是ZK? 對於沒有接觸過ZK的小夥伴,老貓給個非專業但是挺實用的解釋,ZK是一個分散式協調服務,該服務由N多個節點構成,每個節點均可儲存資料。 **資料結構** 在瞭解鎖原理之前我們先來看一下ZK的資料結構,具體如下: ![ZK資料結構](https://img2020.cnblogs.com/blog/2200669/202101/2200669-20210117000546115-582158588.png) 在 Zookeeper 中,每一個數據節點都是一個 ZNode,上圖根目錄下有兩個節點,分別是:app1 和 app2,其中 app1 下面又有三個子節點。那麼我們來看看 ZNode 資料結構到底是什麼樣子的呢。首先我們來了解 ZNode 的型別。 Zookeeper 節點型別可以分為三大類:永續性節點(Persistent)、瞬時性節點(Ephemeral)、順序性節點(Sequential)。現實開發中在建立節點的時候通過組合可以生成以下四種節點型別:持久節點、持久順序節點、瞬時節點、瞬時有序節點。 (1) 持久節點:節點被建立後會一直存在伺服器,直到刪除操作主動清除,這種節點也是最常見的型別。 (2) 持久順序節點:有順序的持久節點,節點特性和持久節點是一樣的,只是額外特性表現在順序上。順序特性實質是在建立節點的時候,會在節點名後面加上一個數字字尾,來表示其順序。 (3) 瞬時節點:會被自動清理掉的節點,它的生命週期和客戶端會話綁在一起,客戶端會話結束,節點會被刪除掉。與永續性節點不同的是,臨時節點不能建立子節點。 (4)瞬時有順序節點:有順序的臨時節點,和持久順序節點相同,在其建立的時候會在名字後面加上數字字尾。 那麼此次我們的ZK分散式鎖就是基於ZK的臨時有序節點實現的,也就是上述的第四種節點。當然光憑藉第四種臨時有序節點是不夠的,我們還需要用到ZK的另外一個比較重要的概念,那就是“ZK觀察器”。 **ZK觀察器** ZK觀察器可以監測到節點的變動,如果節點發生變更會通知到客戶端。我們可以設定觀察器的三個方法:getData(),getChildrean(),exists()。觀察器有一個比較重要的特性就是隻能監控一次,再監控需要重新設定。 **原理流程** (1)利用ZK的瞬時有序節點的特性。 (2)多執行緒併發建立瞬時節點時,得到有序的序列。 (3)序號最小的執行緒獲得鎖。 (4)其他的執行緒則監聽自己節點序號的前一個序號。 (5)前一個執行緒執行完成,刪除自己序號的節點。 (6)下一個序號的執行緒得到通知,繼續執行。 (7)依次類推 通過上述流程大家就會發現,其實在建立節點的時候,就已經確定了執行緒的執行順序。大家看完這個流程可能有點模糊,咱們繼續看下面的圖解,老貓相信大家心裡就會有一個更加清晰的認知。 ![ZK節點監聽執行](https://img2020.cnblogs.com/blog/2200669/202101/2200669-20210117000619620-1117578866.png) 【流程一】我們有四個執行緒,分別是執行緒A、執行緒B、執行緒C、執行緒D。此時執行緒併發執行,這樣就會在我們的ZK中建立四個臨時有序節點,按照先來後到的順序分別是1、2、3、4。此時按照我們流程描述中的第三點描述由於執行緒A對應的序號最小,所以A優先獲取鎖。 【流程二】再依次看第二個流程,此時當A獲取鎖之後,執行緒B的監聽器會去監聽1節點的執行情況,執行緒C的監聽器會去監聽2節點的執行情況,執行緒D的監聽器會去監聽3節點的執行情況依次類推。 【流程三】當執行緒A執行完畢之後會刪除相關的節點1,此時會被執行緒B監聽到,於是執行緒B開始執行,有執行緒C監聽等待著執行緒B節點的釋放,依次類推,直到這四個執行緒都執行完畢。 通過以上的圖解,老貓覺得很多小夥伴對ZK鎖的實現原理應該已經知道了,當然對ZK還是比較陌生的小夥伴也可以專門抽時間去熟悉一下ZK。接下來就和老貓一起來看一下具體的程式碼又是如何實現的吧。 ### 純手擼ZK分散式鎖程式碼 基於上述的流程,我們手擼一下核心的程式碼,首先我們搭建的zk伺服器必須和專案中使用的pom依賴是同一版本,這樣也才能夠避免出問題,由於老貓使用的是zk的3.6.2版本,所以老貓引入的pom如下: ```xml ``` 手寫zk鎖的邏輯主要也是根據上述原理實現,程式碼中有比較晦澀難懂的地方,老貓也寫了詳細的備註,還有不 明白的鐵子可以給老貓留言: ```java /** * @author [email protected] * @date 2021/1/16 10:25 * @公眾號 程式設計師老貓 */ @Slf4j @Service public class ZKLockUtil implements AutoCloseable, Watcher { private ZooKeeper zooKeeper; private String zNode; public ZKLockUtil() throws Exception { this.zooKeeper = new ZooKeeper("localhost:2181",100000,this); } public boolean getLock(String businessCode){ try { // 首先建立業務根節點,類比之前的redis鎖的key以及mysql鎖的businessCode Stat stat = zooKeeper.exists("/"+businessCode,false); if(stat == null){ //表示建立一個業務根目錄,此節點為持久節點,另外的由於在本地搭建的zk沒有設定密碼,所以採用OPEN_ACL_UNSAFE模式 zooKeeper.create("/" +businessCode,businessCode.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } //建立該目錄下的有序瞬時節點,假如我們的訂單業務編號是"order",那麼第一個有序瞬時節點應該是/order/order_0000001 zNode =zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); /** * 按照之前原理的時候的邏輯, * 我們會對所有的節點進行排序並且序號最小的那個節點優先獲取鎖, * 其他節點處於監聽狀態 */ //此處獲取所有子節點,注:之前文章中提及的getData(),getChildrean(),exists()的第二個引數表示是否設定觀察器,ture為設定,false表示不設定