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

程式設計師老貓發表於2021-01-17

前言

這應該是分散式鎖演化的最後一個章節了,相信很多小夥伴們看完這個章節之後在應對高併發的情況下,如何保證執行緒安全心裡肯定也會有譜了。在實際的專案中也可以參考一下老貓的github上的例子,當然程式碼沒有經過特意的封裝,需要小夥伴們自己再好好封裝一下。那麼接下來,就和大家分享一下基於zookeeper的分散式鎖,由於此篇主要分享的是zk的分散式鎖,所以對於zk本身的相關知識點,並不會涉及很多。和分散式鎖實現有關的zk知識點會提及。

Zookeeper實現分散式鎖

何為ZK?(為了打字簡單,後續老貓均以ZK來代替zookeeper),相信很多接觸到Dubbo框架的小夥伴可能聽說過ZK,但是具體也沒有詳細地去學習ZK。那麼又如何利用ZK來實現分散式鎖呢?以下我們一個個來看。

什麼是ZK?

對於沒有接觸過ZK的小夥伴,老貓給個非專業但是挺實用的解釋,ZK是一個分散式協調服務,該服務由N多個節點構成,每個節點均可儲存資料。

資料結構

在瞭解鎖原理之前我們先來看一下ZK的資料結構,具體如下:
ZK資料結構

在 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節點監聽執行

【流程一】我們有四個執行緒,分別是執行緒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如下:

       <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.2</version>
        </dependency>

手寫zk鎖的邏輯主要也是根據上述原理實現,程式碼中有比較晦澀難懂的地方,老貓也寫了詳細的備註,還有不 明白的鐵子可以給老貓留言:

/**
 * @author kdaddy@163.com
 * @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表示不設定
            List<String> childrenNodes = zooKeeper.getChildren("/"+businessCode,false);
            //子節點排序
            Collections.sort(childrenNodes);
            //獲取序號最小的子節點
            String minNode = childrenNodes.get(0);

            //如果建立的節點是最小序號的節點,那麼就獲得鎖
            if(zNode.endsWith(minNode)){
                return true;
            }
            //否則監聽前一個節點的情況
            /**
             * 到這裡說明建立的zNode為第二個或者第三第四個等節點
             * 此處比較晦澀用代入法去理解
             * 如果zNode是第二個節點,那麼監聽的就是第一個最小節點,
             * 如果zNode是第三個節點,那麼此時上一個節點就是迴圈中的當前那個節點。
             * 需要細品
             */
            String lastNode = minNode;
            for (String node : childrenNodes){
                //如果瞬時節點為非第一個節點,那麼監聽前一個節點
                if(zNode.endsWith(node)){
                    zooKeeper.exists("/"+businessCode+"/"+lastNode,true);
                    break;
                }else {
                    lastNode = node;
                }
            }
            //併發情況下wait方法讓出鎖,但是由於併發情景下,為了避免釋放的時候錯亂因此加上synchronized
            synchronized (this){
                wait();
            }
            //當被喚起的時候相當於輪到了,當前拿到了鎖,所以return true
            return true;

        }catch (Exception e){
            e.printStackTrace();
        }
        return false;
    }
    @Override
    public void process(WatchedEvent watchedEvent) {
        //如果監聽到節點被刪除,那麼則會通知下一個執行緒
        if(watchedEvent.getType() == Event.EventType.NodeDeleted){
            synchronized (this){
                notify();
            }
        }
    }
    @Override
    public void close() throws Exception {
        zooKeeper.delete(zNode,-1);
        zooKeeper.close();
        log.info("我已經釋放了鎖!");
    }
}

具體service層的程式碼老貓也做了更改,由於只看鎖,所以在此老貓將相關落訂單的邏輯去除了,對於上述工具類,可以進行如下使用:

/**
 * @author kdaddy@163.com
 * @date 2021/1/16 10:25
 * @公眾號 程式設計師老貓
 */
@Service
@Slf4j
public class ZKLockService {
    @Autowired
    private ZKLockUtil zkLockUtil;
    private String ORDER_KEY = "order_kd";
    public  Integer createOrder() throws Exception{
        log.info("進入了方法");
        try {
            if (zkLockUtil.getLock(ORDER_KEY)) {
                log.info("拿到了鎖");
                //此處為了手動演示併發,所以我們暫時在這裡休眠
                Thread.sleep(6000);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            zkLockUtil.close();
        }
        log.info("方法執行完畢");
        return 1;
    }
}

上述即為實現程式碼,相關的邏輯,老貓也在程式碼的備註中闡釋。如果還有不清楚的小夥伴可以給老貓留言。當然想要完整測試程式碼也可以去老貓的github地址下載。地址:https://github.com/maoba/kd-distribute

curator客戶端的使用

相信有很多還是會有很多小夥伴會說,上述的流程邏輯比較繞,太讓人頭疼了。那麼福利來了,其實關於ZK鎖的話還有可以用封裝比較完善的客戶端,那就是curator。這個客戶端本身就已經實現了ZK的分散式鎖,我們們開箱呼叫即可。如果有更多的小夥伴想要了解curator,也可以去官網去研究一番,具體的地址為:http://curator.apache.org/。當然老貓下面的程式碼也是根據官網的步驟寫出來的。具體程式碼實現如下:

 <dependency>
     <groupId>org.apache.curator</groupId>
     <artifactId>curator-recipes</artifactId>
     <version>4.3.0</version>
</dependency>

由於curator每次啟動都要連線zk,所以老貓乾脆將其放在springboot的啟動中。其實上面手寫的通過構造方法連線zk的方式也可以做一下改造。

/**
 * @author ktdaddy
 * @公眾號 程式設計師老貓
 */
@SpringBootApplication
@MapperScan("com.kd.distribute.dao")
public class DistributeApplication {
    public static void main(String[] args) {
        SpringApplication.run(DistributeApplication.class, args);
    }
    //啟動服務的時候連線zk,並且指定開始使用和結束使用的方法
    @Bean(initMethod="start",destroyMethod = "close")
    public CuratorFramework getCuratorFramework() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
        return client;
    }
}

具體鎖的使用程式碼如下:

/**
 * @author kdaddy@163.com
 * @date 2021/1/16 22:49
 * @公眾號 程式設計師老貓
 */
@Service
@Slf4j
public class CuratorLockService {
    private String ORDER_KEY = "order_kd";
    @Autowired
    private CuratorFramework client;
    public  Integer createOrder() throws Exception{
        log.info("進入了方法");
        InterProcessMutex lock = new InterProcessMutex(client, "/"+ORDER_KEY);
        try {
            if (lock.acquire(30, TimeUnit.SECONDS)) {
                log.info("拿到了鎖");
                //此處為了手動演示併發,所以我們暫時在這裡休眠6s
                Thread.sleep(6000);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                log.info("我釋放了鎖!!");
                lock.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        log.info("方法執行完畢");
        return 1;
    }
}

相當簡單,當然有興趣研究原始碼實現的小夥伴也可以檢視一下InterProcessMutex的相關的原始碼。在此老貓不贅述。

分散式鎖的對比

到此,我們將分散式系統的鎖的解決方案都已經和大家分享過了,最終我們們來進行一個對比,具體如下:

分散式鎖的對比

看了上面這個比較之後,其實在我們的實際專案中,還是推薦現成的 curator 實現方式以及redisson實現方式,因為畢竟目前來說是相當成熟的方案,不推薦由我們自己的程式碼去實現。所以小夥伴們在選擇的時候就不用糾結了。

寫在最後

老貓花了將近半個月的時候整理和輸出了單體鎖演化到分散式鎖的解決方案,熬了比較多的夜,如果能給大家帶來收穫,那是再好不過的了。當然看到這裡也希望能得到你的點贊、關注和轉發。你的支援,是老貓原創的最大動力,後面老貓會帶給大家更多分散式系統的解決方案。也希望能得到你的持續關注。更多精彩歡迎大家關注公眾號“程式設計師老貓”

相關文章