淺談分佈式鎖

爲何要有分佈式鎖

隨着架構系統的演進,由純真的單機架構到容器化編排的分佈式架構,能夠說是一個大型互聯網企業發展的必然走向。在網站初創時,應用數量和用戶較少,能夠把 Tomcat 和Mysql 部署在同一臺機器上。隨着用戶數量增多,訪問量增大,併發升高,Tomcat 和 MySQL 競爭資源,此時,單機已經扛不住了,須要把 Tomcat 和 MySQL 分離在不一樣的機器上,用於提高單臺機器的處理能力。業務歷來沒有減小,產品越作越大。應用也愈來愈複雜,原來的大應用,拆分紅多個小應用,加入各級緩存,作了反向代理負責均衡,最後墜入分庫分表的深淵。html

微服務漸漸代替了龐大冗雜的服務,每一個小服務,各司其職。這時候是否是就不存在資源競爭的問題了呢?答案毋庸置疑,在架構的演進過程當中,無時無刻都存在着資源競爭的問題。java

提及資源競爭的問題,是否是想起了在計算機科學中的一個經典問題——哲學家就餐,也就是在並行計算中多線程同步( Synchronization )時產生的問題?哲學家就餐問題用來解釋死鎖和資源耗盡的問題,咱們不作詳細的討論,感興趣的同窗能夠搜索資料瞭解。既然存在資源競爭的問題,解決的方案必然是對資源加鎖,對於鎖你們確定不陌生,在 Java 中synchronized 關鍵字和 ReentrantLock 可重入鎖在咱們的代碼或者一些開源代碼中隨處可見的,通常用於在本地多線程環境中控制對資源的併發訪問。可是隨着微服務架構的蓬勃興起,分佈式的快速發展,本地加鎖已經不能知足咱們的業務需求,若是還經過本地加鎖的方式鎖定資源,在分佈式環境中是無用的。因而人們爲了在分佈式環境中也能實現本地鎖的效果,也是紛紛各出其招。node

Martin Kleppmann 是英國劍橋大學的分佈式系統的研究員,以前和 Redis 之父 Antirez 進行過關於 RedLock(紅鎖,後續有講到)是否安全的激烈討論。Martin 認爲通常咱們使用分佈式鎖有兩個場景:mysql

  • 效率:使用分佈式鎖能夠避免不一樣節點重複相同的工做,這些工做會浪費資源。好比用戶付了錢以後有可能不一樣節點會發出多封短信。
  • 正確性:加分佈式鎖一樣能夠避免破壞正確性的發生,若是兩個節點在同一條數據上面操做,好比多個節點機器對同一個訂單操做不一樣的流程有可能會致使該筆訂單最後狀態出現錯誤,形成損失。

分佈式鎖的特色

在瞭解分佈式鎖以前,咱們首先要了解操做系統級別的鎖(特指 Linux 系統)和 Java 編髮編程時遇到的鎖。對 Linux 鎖和 Java鎖有大概的瞭解後,咱們深刻分析分佈式鎖的實現機制。若是還想深刻了解 Linux 的鎖相關的信息,可查閱參考文章。linux

linux 鎖的特色

在現代操做系統裏,同一時間可能有多個內核執行命令在執行,所以內核其實像多進程多線程編程同樣也須要一些同步機制來同步各執行單元對共享數據的訪問。尤爲是在多核CPU 系統上,更須要一些同步機制來同步不一樣處理器上的執行單元對共享的數據的訪問。在主流的 Linux 內核中包含了幾乎全部現代的操做系統具備的同步機制,這些同步機制包括:算法

  • Atomic(原子操做): 所謂原子操做,就是該操做毫不會在執行完畢前被任何其餘任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位。
  • Mutex(互斥量): 互斥鎖主要用於實現內核中的互斥訪問功能。內核互斥鎖是在原子 API 之上實現的,但這對於內核用戶是不可見的。對它的訪問必須遵循一些規則:同一時間只能有一個任務持有互斥鎖,並且只有這個任務能夠對互斥鎖進行解鎖。互斥鎖不能進行遞歸鎖定或解鎖。一個互斥鎖對象必須經過其API初始化,而不能使用memset或複製初始化。一個任務在持有互斥鎖的時候是不能結束的。互斥鎖所使用的內存區域是不能被釋放的。使用中的互斥鎖是不能被從新初始化的。而且互斥鎖不能用於中斷上下文。可是互斥鎖比當前的內核信號量選項更快,而且更加緊湊,所以若是它們知足您的需求,那麼它們將是您明智的選擇。可是,對於互斥鎖而言,若是資源已經被佔用,其它的資源申請進程只能進入 sleep 狀態。
  • Spinlock(自旋鎖): 自旋鎖與互斥鎖有點相似,只是自旋鎖不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,」自旋」一詞就是所以而得名。因爲自旋鎖使用者通常保持鎖時間很是短,所以選擇自旋而不是睡眠是很是必要的,自旋鎖的效率遠高於互斥鎖。
  • rwlock(讀寫鎖): 讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分紅讀者和寫者,讀者只對共享資源進行讀訪問,寫者則須要對共享資源進行寫操做。這種鎖相對於自旋鎖而言,能提升併發性,由於在多處理器系統中,它容許同時有多個讀者來訪問共享資源,最大可能的讀者數爲實際的邏輯 CPU 數。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數相關),但不能同時既有讀者又有寫者。
  • semaphore(信號量): 信號量在建立時須要設置一個初始值,表示同時能夠有幾個任務能夠訪問該信號量保護的共享資源,初始值爲1就變成互斥鎖(Mutex),即同時只能有一個任務能夠訪問信號量保護的共享資源。一個任務要想訪問共享資源,首先必須獲得信號量,獲取信號量的操做將把信號量的值減1,若當前信號量的值爲負數,代表沒法得到信號量,該任務必須掛起在該信號量的等待隊列等待該信號量可用;若當前信號量的值爲非負數,表示能夠得到信號量,於是能夠馬上訪問被該信號量保護的共享資源。當任務訪問完被信號量保護的共享資源後,必須釋放信號量,釋放信號量經過把信號量的值加1實現,若是信號量的值爲非正數,代表有任務等待當前信號量,所以它也喚醒全部等待該信號量的任務。
  • rw_semaphore(讀寫信號量): 讀寫信號量對訪問者進行了細分,或者爲讀者,或者爲寫者,讀者在保持讀寫信號量期間只能對該讀寫信號量保護的共享資源進行讀訪問,若是一個任務除了須要讀,可能還須要寫,那麼它必須被歸類爲寫者,它在對共享資源訪問以前必須先得到寫者身份,寫者在發現本身不須要寫訪問的狀況下能夠降級爲讀者。
  • 條件變量
  • seqlock(順序鎖): 順序鎖也是對讀寫鎖的一種優化,對於順序鎖,讀者毫不會被寫者阻塞,也就說,讀者能夠在寫者對被順序鎖保護的共享資源進行寫操做時仍然能夠繼續讀,而沒必要等待寫者完成寫操做,寫者也不須要等待全部讀者完成讀操做纔去進行寫操做。可是,寫者與寫者之間仍然是互斥的,即若是有寫者在進行寫操做,其餘寫者必須自旋在那裏,直到寫者釋放了順序鎖。
  • BKL(大內核鎖): 大內核鎖本質上也是自旋鎖,可是它又不一樣於自旋鎖,自旋鎖是不能夠遞歸得到鎖的,由於那樣會致使死鎖。但大內核鎖能夠遞歸得到鎖。大內核鎖用於保護整個內核,而自旋鎖用於保護很是特定的某一共享資源。進程保持大內核鎖時能夠發生調度,具體實現是:在執行 schedule 時,schedule 將檢查進程是否擁有大內核鎖,若是有,它將被釋放,以至於其它的進程可以得到該鎖,而當輪到該進程運行時,再讓它從新得到大內核鎖。注意在保持自旋鎖期間是不容許發生調度的。
  • brlock(大讀者鎖): 大讀者鎖是讀寫鎖的高性能版,讀者能夠很是快地得到鎖,但寫者得到鎖的開銷比較大。大讀者鎖只存在於 2.4 內核中,在 2.6 中已經沒有這種鎖(提醒讀者特別注意)。它們的使用與讀寫鎖的使用相似,只是全部的大讀者鎖都是事先已經定義好的。這種鎖適合於讀多寫少的狀況,它在這種狀況下遠好於讀寫鎖。
  • RCU(Read-Copy Update): 顧名思義就是讀-拷貝修改,它是基於其原理命名的。對於被RCU保護的共享數據結構,讀者不須要得到任何鎖就能夠訪問它,但寫者在訪問它時首先拷貝一個副本,而後對副本進行修改,最後使用一個回調(callback)機制在適當的時機把指向原來數據的指針從新指向新的被修改的數據。這個時機就是全部引用該數據的CPU都退出對共享數據的操做。

Java鎖的特色

在不少書寫Java併發的文章中,咱們常常看到有這些鎖的概念。這些概念中,並不全指鎖的狀態,有的是指所得特性,有的是指所得設計。本文僅僅簡要敘述鎖的概念,不過多涉及Java鎖的實現,這部份內容放在《Javaer不得不說的 Java 「鎖」事》一文中。sql

  • 公平鎖 / 非公平鎖:按照多線程申請資源是否按照順序來獲取鎖。
  • 可重入鎖 / 不可重入鎖:廣義上的可重入鎖指的是可重複可遞歸調用的鎖,在外層使用鎖以後,在內層仍然可使用,而且不發生死鎖(前提得是同一個對象或者 class),這樣的鎖就叫作可重入鎖,不然就叫不可重入鎖。 ReentrantLock 和 synchronized都是可重入鎖。
  • 獨享鎖 / 共享鎖:數據庫

    • 獨享鎖該鎖每一次只能被一個線程所持有。
    • 共享鎖該鎖可被多個線程共有,典型的就是ReentrantReadWriteLock裏的讀鎖,它的讀鎖是能夠被共享的,可是它的寫鎖確每次只能被獨佔。
  • 互斥鎖 / 讀寫鎖:apache

    • 在訪問共享資源以前對進行加鎖操做,在訪問完成以後進行解鎖操做。加鎖後,任何其餘試圖再次加鎖的線程會被阻塞,直到當前進程解鎖。這是互斥鎖。
    • 讀寫鎖既是互斥鎖,又是共享鎖,read 模式是共享,write 是互斥(排它鎖)的。
  • 樂觀鎖 / 悲觀鎖:編程

    • 悲觀鎖老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖, Java中 synchronized 和 ReentrantLock 等獨佔鎖就是悲觀鎖思想的實現。
    • 樂觀鎖老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於 write_condition 機制,其實都是提供的樂觀鎖。在 Java 中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
  • 分段鎖:分段鎖實際上是一種鎖的設計,並非具體的一種鎖,對於 ConcurrentHashMap 而言,其併發的實現就是經過分段鎖的形式來實現高效的併發操做。
  • 偏向鎖 / 輕量級鎖 / 重量級鎖:

    • 偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價。
    • 輕量級鎖是指當鎖是偏向鎖的時候,被另外一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。
    • 重量級鎖是指當鎖爲輕量級鎖的時候,另外一個線程雖然是自旋,但自旋不會一直持續下去,當自旋必定次數的時候,尚未獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其餘申請的線程進入阻塞,性能下降。
  • 自旋鎖:是指當一個線程在獲取鎖的時候,若是鎖已經被其它線程獲取,那麼該線程將循環等待,而後不斷的判斷鎖是否可以被成功獲取,直到獲取到鎖纔會退出循環。

分佈式鎖的特色

對系統內核鎖和Java鎖有初步的瞭解以後,咱們總結髮現,所必需的要有如下特色:

  • 互斥性: 互斥性是最基本的特徵,分佈式鎖須要保證在不一樣節點的不一樣線程的互斥。
  • 可重入性: 同一個節點上的同一個線程若是獲取了鎖以後那麼也能夠再次獲取這個鎖。
  • 鎖超時: 和本地鎖同樣支持鎖超時,防止死鎖。
  • 高效,高可用: 加鎖和解鎖須要高效,同時也須要保證高可用防止分佈式鎖失效,能夠增長降級。
  • 支持阻塞和非阻塞: 和 ReentrantLock 同樣支持 lock 和 trylock 以及 tryLock(long timeOut)。
  • 支持公平鎖和非公平鎖(可選):公平鎖的意思是按照請求加鎖的順序得到鎖,非公平鎖就相反是無序的。這個通常來講實現的比較少。

常見分佈式鎖

通常實現分佈式鎖有如下幾個方式:

  • MySql
  • Zk
  • Redis
  • Etcd
  • 自研分佈式鎖:如谷歌的Chubby。

下面就 MySQL 和 zk curator 客戶端加鎖的實現方式逐一列舉,關於 Redis、Zk 原生客戶端、etcd 等其餘方式的分佈式鎖的實現原理,放在後面的章節。

MySQL分佈式鎖

MySQL實現分佈式鎖相對簡單,建立一張鎖資源表。

CREATE TABLE resource_lock (
    `id` BIGINT(20) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `resource_name` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '資源名稱',
    `node_info` VARCHAR(128) NULL DEFAULT NULL COMMENT '',
    `count` INT(10) NOT NULL DEFAULT '0' COMMENT '',
    `description` VARCHAR(128) NULL DEFAULT NULL COMMENT '',
    `gmt_create` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '',
    `gmt_modify` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
    UNIQUE KEY `uk_resource` (`resource_name`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '資源加鎖表';

前面分佈式鎖所說的 lock(),trylock(long timeout),trylock() 這幾個方法能夠用下面的僞代碼實現。

lock()

lock通常是阻塞式的獲取鎖,阻塞知道獲取到鎖或者異常結束,那麼咱們能夠寫一個死循環來執行其操做:

public void lock() {
    while(true) {
        if (mysqlLock.lock(resoureName)) {
            return;
        }
        // 休眠3ms後重試
        LockSuprot.parkNanos(1000*1000*3);
    }
}

mysqlLock.lcok() 內部是一個SQL,爲了達到可重入鎖的效果那麼咱們應該先進行查詢,若是有值,那麼須要比較 node_info 是否一致,這裏的 node_info 能夠用機器 IP 和線程名字來表示,若是一致那麼就加可重入鎖 count 的值,若是不一致那麼就返回false。若是沒有值那麼直接插入一條數據。僞代碼以下:

public class MysqlLock {
    @Resource
    private MysqlLockMapper mysqlLockMapper;
    private NodeInfo nodeInfo;

    public MysqlLock(NodeInfo nodeInfo) {
        this.nodeInfo = nodeInfo;
    }

    @Transcation
    public boolean lock(String resourceName) {
        MyResource result = mysqlLockMapper.existsResource(resourceName);
        if (result != null) {
            if (Objects.equeals(nodeInfo, result.getNodeInfo())) {
                mysqlLockMapper.updateResourceCount(resourceName);
                return true;
            } else {
                return false;
            }
        } else {
            mysqlLockMapper.insertResource(resourceName, nodeInfo);
        }
    }
}

須要注意的是這一段代碼須要加事務,必需要保證這一系列操做的原子性。

上面代碼對應的三條 Mybatis 版的SQL語句以下:

-- mysqlLockMapper.existsResource

select * from resource_lock where resource_name = ${resourceName} for update

-- mysqlLockMapper.updateResourceCount

update resource_lock set count = count + 1 where resource_name = ${resourceName}

-- mysqlLockMapper.insertResource

insert into resource_lock(`resource_name`,`node_info`,`count`,`description`)
values(#{resourceName}, ${nodeInfo}, 1, '')

trylock()

tryLock() 是非阻塞獲取鎖,若是獲取不到那麼就會立刻返回,代碼能夠以下:

public boolean tryLock() {
    return mysqlLock.lock(resourceName);
}

trylock(long millsecs)

public boolean trylock(long millsecs) {
    // 記錄超時時間
    long deadline = System.currentTimeMillis() + millsecs;
    while(true) {
        if (mysqlLock.tryLock()) {
            return true;
        }
        deadline = deadline - millsecs;

        // 避免網絡延遲引發加鎖失敗,增長自旋超時閾值,可設置爲300ms
        if (deadline <= spinTimeoutThreshold) {
            return false;
        }
        if (millsecs <= 0) {
            return false;
        }
    }
}

mysqlLock.lock 和上面同樣,可是要注意的是 select … for update 這個是阻塞的獲取行鎖,若是同一個資源併發量較大仍是有可能會退化成阻塞的獲取鎖。

unlock()

public boolean unlock() {
    MyResource result = mysqlLockMapper.existsResource(resourceName);
    if (result != null) {
        if (Objects.equeals(nodeInfo, result.getNodeInfo())) {
            if (result.getCount() > 1) {
                // count - 1 
                mysqlLockMapper.decrementResource(resourceName);
            } else {
                mysqlLockMapper.deleteResource(resourceName);
            }
        } else {
            return false;
        }
    } else {
        return false;
    }
}

上面新增兩條 Mybatis 版本的SQL語句:

-- mysqlLockMapper.decrementResource(resourceName)

update resource_lock set count = count - 1 where resource_name = ${resourceName}

-- mysqlLockMapper.deleteResource(resourceName)

delete from resource_lock where resource_name = ${resourceName}

鎖超時

咱們注意到,鎖的釋放是經過 delete 語句刪除資源鎖的,若是加鎖的客戶端因爲某些緣由掛掉了,鎖就一直存在。這時,咱們能夠經過定時任務,在加鎖的時候添加任務到任務系統,也能夠經過定時任務檢查釋放鎖。

ZK客戶端Curator分佈式鎖

ZooKeeper也是咱們常見的實現分佈式鎖方法,ZooKeeper 是以 Paxos 算法爲基礎分佈式應用程序協調服務。Zk 的數據節點和文件目錄相似,因此咱們能夠用此特性實現分佈式鎖。咱們以某個資源爲目錄,而後這個目錄下面的節點就是咱們須要獲取鎖的客戶端,未獲取到鎖的客戶端註冊須要註冊 Watcher 到上一個客戶端,能夠用下圖表示。

/lock是咱們用於加鎖的目錄,/resource_name是咱們鎖定的資源,其下面的節點按照咱們加鎖的順序排列。

Curator 封裝了 Zookeeper 底層的 API,使咱們更加容易方便的對 Zookeeper 進行操做,而且它封裝了分佈式鎖的功能,這樣咱們就不須要再本身實現了。

Curator 實現了可重入鎖(InterProcessMutex),也實現了不可重入鎖(InterProcessSemaphoreMutex)。在可重入鎖中還實現了讀寫鎖。

Curator-Recipes實現了五種分佈式鎖:

下面就分佈式可重入鎖舉例。

可重入鎖InterProcessMutex

InterProcessMutex 是 Curator 實現的可重入鎖,建立 InterProcessMutex 實例
InterProcessMutex 提供了兩個構造方法,傳入一個 CuratorFramework 實例和一個要使用的節點路徑,InterProcessMutex 還容許傳入一個自定義的驅動類,默認是使用 StandardLockInternalsDriver。

public InterProcessMutex(CuratorFramework client, String path);
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver);

獲取鎖

使用 acquire 方法獲取鎖, acquire 方法有兩種:

public void acquire() throws Exception;

獲取鎖,一直阻塞到獲取到鎖爲止。獲取鎖的線程在獲取鎖後仍然能夠調用 acquire() 獲取鎖(可重入)。 鎖獲取使用完後,調用了幾回 acquire(),就得調用幾回 release() 釋放。

public boolean acquire(long time, TimeUnit unit) throws Exception;

與 acquire()相似,等待 time * unit 時間獲取鎖,若是仍然沒有獲取鎖,則直接返回 false。

  • 共享資源
public class FakeLimitedResource {

    //總共250張火車票
    private Integer ticket = 250;

    public void use() throws InterruptedException {
        try {
            System.out.println("火車票還剩"+(--ticket)+"張!");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 使用鎖操做資源
public class ExampleClientThatLocks {

    /** 鎖 */
    private final InterProcessMutex lock;
    /** 共享資源 */
    private final FakeLimitedResource resource;
    /** 客戶端名稱 */
    private final String clientName;

    public ExampleClientThatLocks(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName) {
        this.resource = resource;
        this.clientName = clientName;
        lock = new InterProcessMutex(client, lockPath);
    }

    public void doWork(long time, TimeUnit unit) throws Exception {
        if ( !lock.acquire(time, unit) ) {
            throw new IllegalStateException(clientName + " could not acquire the lock");
        }
        try {
            System.out.println(clientName + " has the lock");
            //操做資源
            resource.use();
        } finally {
            System.out.println(clientName + " releasing the lock");
            lock.release(); //老是在Final塊中釋放鎖。
        }
    }
}
  • 客戶端
public class LockingExample {
    private static final int QTY = 5; // 併發操做線程數
    private static final int REPETITIONS = QTY * 10; // 資源總量
    private static final String CONNECTION_STRING = "127.0.0.1:2181";
    private static final String PATH = "/locks";

    public static void main(String[] args) throws Exception {

        //FakeLimitedResource模擬某些外部資源,這些外部資源一次只能由一個進程訪問
        final FakeLimitedResource resource = new FakeLimitedResource();

        ExecutorService service = Executors.newFixedThreadPool(QTY);
        try {
            for ( int i = 0; i < QTY; ++i ){
                final int index = i;
                Callable<Void>  task = new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        CuratorFramework client = CuratorFrameworkFactory.newClient(CONNECTION_STRING, new ExponentialBackoffRetry(1000, 3,Integer.MAX_VALUE));
                        try {
                            client.start();
                            ExampleClientThatLocks example = new ExampleClientThatLocks(client, PATH, resource, "Client " + index);
                            for ( int j = 0; j < REPETITIONS; ++j ) {
                                example.doWork(10, TimeUnit.SECONDS);
                            }
                        }catch ( InterruptedException e ){
                            Thread.currentThread().interrupt();
                        }catch ( Exception e ){
                            e.printStackTrace();
                        }finally{
                            CloseableUtils.closeQuietly(client);
                        }
                        return null;
                    }
                };
                service.submit(task);
            }

            service.shutdown();
            service.awaitTermination(10, TimeUnit.MINUTES);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

起五個線程,即五個窗口賣票,五個客戶端分別有50張票能夠賣,先是嘗試獲取鎖,操做資源後,釋放鎖。

加鎖的流程具體以下:

  1. 首先進行可重入的斷定:這裏的可重入鎖記錄在 ConcurrentMap,threadData 這個 Map 裏面,若是threadData.get(currentThread)是有值的那麼就證實是可重入鎖,而後記錄就會加1。咱們以前的 Mysql 其實也能夠經過這種方法去優化,能夠不須要 count 字段的值,將這個維護在本地能夠提升性能。
  2. 而後在咱們的資源目錄下建立一個節點:好比這裏建立一個 /0000000002 這個節點,這個節點須要設置爲 EPHEMERAL_SEQUENTIAL 也就是臨時節點而且有序。
  3. 獲取當前目錄下全部子節點,判斷本身的節點是否位於子節點第一個。
  4. 若是是第一個,則獲取到鎖,那麼能夠返回。
  5. 若是不是第一個,則證實前面已經有人獲取到鎖了,那麼須要獲取本身節點的前一個節點。/0000000002 的前一個節點是 /0000000001,咱們獲取到這個節點以後,再上面註冊Watcher(這裏的 watcher 其實調用的是 object.notifyAll(),用來解除阻塞)。
  6. object.wait(timeout) 或 object.wait() :進行阻塞等待這裏和咱們第5步的watcher相對應。

釋放鎖

線程經過 acquire() 獲取鎖時,可經過 release()進行釋放,若是該線程屢次調用了 acquire() 獲取鎖,則若是隻調用一次 release() 該鎖仍然會被該線程持有。

note:同一個線程中InterProcessMutex實例是可重用的,也就是不須要在每次獲取鎖的時候都new一個InterProcessMutex實例,用同一個實例就好。

解鎖的具體流程:

  1. 首先進行可重入鎖的斷定: 若是有可重入鎖只須要次數減 1 便可,減1以後加鎖次數爲 0 的話繼續下面步驟,不爲 0 直接返回。
  2. 刪除當前節點。
  3. 刪除 threadDataMap 裏面的可重入鎖的數據。

讀寫鎖

Curator提供了讀寫鎖,其實現類是 InterProcessReadWriteLock,這裏的每一個節點都會加上前綴:

private static final String READ_LOCK_NAME  = "__READ__";
private static final String WRITE_LOCK_NAME = "__WRIT__";

根據不一樣的前綴區分是讀鎖仍是寫鎖,對於讀鎖,若是發現前面有寫鎖,那麼須要將 watcher 註冊到和本身最近的寫鎖。寫鎖的邏輯和咱們以前分析的依然保持不變。

鎖超時

Zookeeper不須要配置鎖超時,因爲咱們設置節點是臨時節點,咱們的每一個機器維護着一個ZK的session,經過這個session,ZK能夠判斷機器是否宕機。若是咱們的機器掛掉的話,那麼這個臨時節點對應的就會被刪除,因此咱們不須要關心鎖超時。

分佈式鎖的安全問題

  • 長時間的GC pause: 作Java開發的同窗確定對 GC 不陌生,在 GC 的時候會發生STW(stop-the-world),例如CMS垃圾回收器,會有兩個階段進行 STW 防止引用繼續進行變化。Martin反駁Redlock的文章《How to do distributed locking》中對此有詳細的解釋,下面此圖來源此文:

    client1 獲取了鎖而且設置了鎖的超時時間,可是 client1 以後出現了 STW,這個 STW 時間比較長,致使分佈式鎖進行了釋放,client2 獲取到了鎖,這個時候 client1 恢復了鎖,那麼就會出現 client1,client2 同時獲取到鎖,這個時候分佈式鎖不安全問題就出現了。這個其實不只僅侷限於 RedLock,對於咱們的 ZK,Mysql 同樣的有一樣的問題。

  • 時鐘發生跳躍:對於Redis服務器若是其時間發生了跳躍,那麼確定會影響咱們鎖的過時時間,那麼咱們的鎖過時時間就不是咱們預期的了,也會出現 client1 和 client2 獲取到同一把鎖,那麼也會出現不安全,這個對於 Mysql 也會出現。可是 ZK 因爲沒有設置過時時間,那麼發生跳躍也不會受影響。
  • 長時間的網絡I/O:這個問題和咱們的 GC 的 STW 很像,也就是咱們這個獲取了鎖以後咱們進行網絡調用,其調用時間可能比咱們鎖的過時時間都還長,那麼也會出現不安全的問題,這個 Mysql 也會有,ZK 也會出現這個問題。

GC的STW

對於這個問題能夠看見基本全部的都會出現問題,Martin 給出了一個解法,對於 ZK 這種他會生成一個自增的序列,那麼咱們真正進行對資源操做的時候,須要判斷當前序列是不是最新,有點相似於咱們樂觀鎖。固然這個解法Redis做者進行了反駁,你既然都能生成一個自增的序列了那麼你徹底不須要加鎖了,也就是能夠按照相似於Mysql樂觀鎖的解法去作。

時鐘發生跳躍

Martin 以爲 RedLock 不安全很大的緣由也是由於時鐘的跳躍,由於鎖過時強依賴於時間,可是 ZK 不須要依賴時間,依賴每一個節點的 Session。Redis做者也給出瞭解答:對於時間跳躍分爲人爲調整和 NTP 自動調整。

  • 人爲調整:人爲調整影響的那麼徹底能夠人爲不調整,這個是處於可控的。
  • NTP自動調整: 這個能夠經過必定的優化,把跳躍時間控制的可控範圍內,雖然會跳躍,可是是徹底能夠接受的。

長時間的網絡I/O

這一塊不是他們討論的重點,我本身以爲,對於這個問題的優化能夠控制網絡調用的超時時間,把全部網絡調用的超時時間相加,那麼咱們鎖過時時間其實應該大於這個時間,固然也能夠經過優化網絡調用好比串行改爲並行,異步化等。能夠參考下面兩篇文章:
並行化-你的高併發大殺器異步化-你的高併發大殺器

參考文章

  1. Linux內核中的各類鎖
  2. linux幾種鎖的分析與比較
  3. 鎖的種類與特色
  4. 聊聊分佈式鎖
  5. zookeeper開源客戶端Curator典型應用場景之-分佈式鎖

該文首發《虛懷若谷》我的博客,轉載前請務必署名,轉載請標明出處。

古之善爲道者,微妙玄通,深不可識。夫惟不可識,故強爲之容:

豫兮若冬涉川,猶兮若畏四鄰,儼兮其若客,渙兮若冰之釋,敦兮其若樸,曠兮其若谷,混兮其若濁。

孰能濁以靜之徐清?孰能安以動之徐生?

保此道不欲盈。夫惟不盈,故能敝而新成。

請關注個人微信公衆號:下雨就像彈鋼琴,Thanks♪(・ω・)ノ
微信二維碼

相關文章
相關標籤/搜索