【分散式鎖的演化】終章!手擼ZK分散式鎖!
阿新 • • 發佈:2021-01-17
### 前言
這應該是分散式鎖演化的最後一個章節了,相信很多小夥伴們看完這個章節之後在應對高併發的情況下,如何保證執行緒安全心裡肯定也會有譜了。在實際的專案中也可以參考一下老貓的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表示不設定