Zookeeper 分佈式鎖 (圖解+秒懂+史上最全)

文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈爲小夥伴奉上如下珍貴的學習資源:html


推薦: 瘋狂創客圈 高質量 博文

高併發 必讀 的精彩博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
Zookeeper 分佈式鎖 (圖解+秒懂+史上最全) Webflux(史上最全)
SpringCloud gateway (史上最全) TCP/IP(圖解+秒懂+史上最全)
10分鐘看懂, Java NIO 底層原理 Feign原理 (圖解)
更多精彩博文 ..... 請參見【 瘋狂創客圈 高併發 總目錄

史上最全 Java 面試題 28 專題 總目錄

精心梳理、吐血推薦、史上最強、建議收藏 阿里、京東、美團、頭條.... 隨意挑、橫着走!!!
1.Java算法面試題(史上最強、持續更新、吐血推薦) 2.Java基礎面試題(史上最全、持續更新、吐血推薦)
3.JVM面試題(史上最強、持續更新、吐血推薦) 四、架構設計面試題 (史上最全、持續更新、吐血推薦)
五、Spring面試題 專題 六、SpringMVC面試題 專題
7.SpringBoot - 面試題(史上最強、持續更新) 八、Tomcat面試題 專題部分
9.網絡協議面試題(史上最全、持續更新、吐血推薦) 十、TCP/IP協議(圖解+秒懂+史上最全)
11.JUC併發包與容器 - 面試題(史上最強、持續更新) 十二、設計模式面試題 (史上最全、持續更新、吐血推薦)
13.死鎖面試題(史上最強、持續更新) 15.Zookeeper 分佈式鎖 (圖解+秒懂+史上最全)
1四、Redis 面試題 - 收藏版(史上最強、持續更新) 1六、Zookeeper 面試題(史上最強、持續更新)
1七、分佈式事務面試題 (史上最全、持續更新、吐血推薦) 1八、一致性協議 (史上最全)
1九、Zab協議 (史上最全) 20、Paxos 圖解 (秒懂)
2一、raft 圖解 (秒懂) 2六、消息隊列、RabbitMQ、Kafka、RocketMQ面試題 (史上最全、持續更新)
22.Linux面試題(史上最全、持續更新、吐血推薦) 2三、Mysql 面試題(史上最強、持續更新)
2四、SpringCloud 面試題 - 收藏版(史上最強、持續更新) 2五、Netty 面試題 (史上最強、持續更新)
2七、內存泄漏 內存溢出(史上最全) 2八、JVM 內存溢出 實戰 (史上最全)

面試必備:分佈式鎖原理與實戰

在單體的應用開發場景中,涉及併發同步的時候,你們每每採用synchronized或者Lock的方式來解決多線程間的同步問題。但在分佈式集羣工做的開發場景中,那麼就須要一種更加高級的鎖機制,來處理種跨JVM進程之間的數據同步問題,這就是分佈式鎖。java

公平鎖和可重入鎖的原理

最經典的分佈式鎖是可重入的公平鎖。什麼是可重入的公平鎖呢?直接講解的概念和原理,會比較抽象難懂,仍是從具體的實例入手吧!這裏用一個簡單的故事來類比,估計就簡單多了。node

故事發生在一個沒有自來水的古代,在一個村子有一口井,水質很是的好,村民們都搶着取井裏的水。井就那麼一口,村裏的人不少,村民爲爭搶取水打架鬥毆,甚至頭破血流。程序員

問題老是要解決,因而村長絞盡腦汁,最終想出了一個憑號取水的方案。井邊安排一個看井人,維護取水的秩序。取水秩序很簡單:面試

(1)取水以前,先取號;算法

(2)號排在前面的,就能夠先取水;sql

(3)先到的排在前面,那些後到的,一個一個挨着,在井邊排成一隊。apache

取水示意圖,如圖10-3所示。
在這裏插入圖片描述編程

圖10-3 排隊取水示意圖設計模式

這種排隊取水模型,就是一種鎖的模型。排在最前面的號,擁有取水權,就是一種典型的獨佔鎖。另外,先到先得,號排在前面的人先取到水,取水以後就輪到下一個號取水,挺公平的,說明它是一種公平鎖。

什麼是可重入鎖呢?
假定,取水時以家庭爲單位,家庭的某人拿到號,其餘的家庭成員過來打水,這時候不用再取號,如圖10-4所示。
在這裏插入圖片描述

圖10-4 同一家庭的人不須要重複排隊

圖10-4中,排在1號的家庭,老公取號,假設其老婆來了,直接排第一個,正所謂妻憑夫貴。再看上圖的2號,父親正在打水,假設其兒子和女兒也到井邊了,直接排第二個,所謂子憑父貴。總之,若是取水時以家庭爲單位,則同一個家庭,能夠直接複用排號,不用從後面排起從新取號。

以上這個故事模型中,取號一次,能夠用來屢次取水,其原理爲可重入鎖的模型。在重入鎖模型中,一把獨佔鎖,能夠被屢次鎖定,這就叫作可重入鎖。

ZooKeeper分佈式鎖的原理

理解了經典的公平可重入鎖的原理後,再來看在分佈式場景下的公平可重入鎖的原理。經過前面的分析,基本能夠斷定: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內部自行維護的一個節點序號。

客戶端A發起一個加鎖請求

好比說,第一個客戶端來搞一個順序節點,zk內部會給起個名字叫作:xxx-000001。而後第二個客戶端來搞一個順序節點,zk可能會起個名字叫作:xxx-000002。你們注意一下,最後一個數字都是依次遞增的,從1開始逐次遞增。zk會維護這個順序。

因此這個時候,假如說客戶端A先發起請求,就會搞出來一個順序節點,你們看下面的圖,Curator框架大概會弄成以下的樣子:

在這裏插入圖片描述

你們看,客戶端A發起一個加鎖請求,先會在你要加鎖的node下搞一個臨時順序節點,這一大坨長長的名字都是Curator框架本身生成出來的。

而後,那個最後一個數字是"1"。你們注意一下,由於客戶端A是第一個發起請求的,因此給他搞出來的順序節點的序號是"1"。

接着客戶端A建立完一個順序節點。還沒完,他會查一下"my_lock"這個鎖節點下的全部子節點,而且這些子節點是按照序號排序的,這個時候他大概會拿到這麼一個集合:

img

接着客戶端A會走一個關鍵性的判斷,就是說:唉!兄弟,這個集合裏,我建立的那個順序節點,是否是排在第一個啊?

若是是的話,那我就能夠加鎖了啊!由於明明我就是第一個來建立順序節點的人,因此我就是第一個嘗試加分佈式鎖的人啊!

bingo!加鎖成功!你們看下面的圖,再來直觀的感覺一下整個過程。

在這裏插入圖片描述

客戶端B過來排隊

接着假如說,客戶端A都加完鎖了,客戶端B過來想要加鎖了,這個時候他會幹同樣的事兒:先是在"my_lock"這個鎖節點下建立一個臨時順序節點,此時名字會變成相似於:

img

你們看看下面的圖:

在這裏插入圖片描述

客戶端B由於是第二個來建立順序節點的,因此zk內部會維護序號爲"2"。

接着客戶端B會走加鎖判斷邏輯,查詢"my_lock"鎖節點下的全部子節點,按序號順序排列,此時他看到的相似於:

img

同時檢查本身建立的順序節點,是否是集合中的第一個?

明顯不是啊,此時第一個是客戶端A建立的那個順序節點,序號爲"01"的那個。因此加鎖失敗!

客戶端B開啓監聽客戶端A

加鎖失敗了之後,客戶端B就會經過ZK的API對他的順序節點的上一個順序節點加一個監聽器。zk自然就能夠實現對某個節點的監聽。

若是你們還不知道zk的基本用法,能夠百度查閱,很是的簡單。客戶端B的順序節點是:

img

他的上一個順序節點,不就是下面這個嗎?

img

即客戶端A建立的那個順序節點!

因此,客戶端B會對:

img

這個節點加一個監聽器,監聽這個節點是否被刪除等變化!你們看下面的圖。

在這裏插入圖片描述

接着,客戶端A加鎖以後,可能處理了一些代碼邏輯,而後就會釋放鎖。那麼,釋放鎖是個什麼過程呢?

其實很簡單,就是把本身在zk裏建立的那個順序節點,也就是:

img

這個節點給刪除。

刪除了那個節點以後,zk會負責通知監聽這個節點的監聽器,也就是客戶端B以前加的那個監聽器,說:兄弟,你監聽的那個節點被刪除了,有人釋放了鎖。

在這裏插入圖片描述

此時客戶端B的監聽器感知到了上一個順序節點被刪除,也就是排在他以前的某個客戶端釋放了鎖。

客戶端B搶鎖成功

此時,就會通知客戶端B從新嘗試去獲取鎖,也就是獲取"my_lock"節點下的子節點集合,此時爲:

img

集合裏此時只有客戶端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()方法的大體流程是:首先嚐試着去加鎖,若是加鎖失敗就去等待,而後再重複。

1.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;
    }



//...省略其餘的方法

}

2.tryLock()嘗試加鎖

嘗試加鎖的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
鎖定判斷方法。

3.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()等待方法,執行奪鎖失敗之後的等待邏輯。

4.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加鎖的算法,還差一點就介紹完成。這一點,就是實現鎖的可重入。

5.可重入的實現代碼

什麼是可重入呢?只須要保障同一個線程進入加鎖的代碼,能夠重複加鎖成功便可。
修改前面的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節點。以上代碼,若是要擴展爲多個分佈式鎖的版本,還須要進行簡單改造,這種改造留給各位本身去練習和實現吧。

實戰:curator的InterProcessMutex 可重入鎖

分佈式鎖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分佈式鎖的優勢和缺點

總結一下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 高併發實戰》 圖書簡介 - 瘋狂創...

分佈式利器Zookeeper(二):分佈式鎖 - 簡書

ZooKeeper分佈式鎖簡單實踐 | 老爺爺的博客園

zookeeper實現分佈式鎖_java_腳本之家

基於Zookeeper分佈式鎖實現 - SegmentFault 思否

分佈式鎖用 Redis 仍是 Zookeeper - 知乎

ZooKeeper分佈式鎖的實現原理 - 菜鳥奮鬥史 - 博客園

相關文章
相關標籤/搜索