分佈式鎖原理及經常使用實現

原由

前段時間,看到redis做者發佈的一篇文章《Is Redlock safe?》,Redlock是redis做者基於redis設計的分佈式鎖的算法。文章原由是有一位分佈式的專家寫了一篇文章《How to do distributed locking》,質疑Redlock的正確性。redis做者則在《Is Redlock safe?》文章中給予迴應,一來一回甚是精彩。文本就爲讀者一一解析兩位專家的爭論。html

在瞭解兩位專家的爭論前,讓我先從我瞭解的分佈式鎖一一道來。文章中提到的分佈式鎖均爲排他鎖。java

數據庫鎖表

我第一次接觸分佈式鎖用的是mysql的鎖表。當時我並無分佈式鎖的概念。只知道當時有兩臺交易中心服務器處理相同的業務,每一個交易中心處理訂單的時候須要保證另外一個沒法處理。因而用mysql的一張表來控制共享資源。表結構以下:node

CREATE TABLE `lockedOrder` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主碼',
  `type` tinyint(8) unsigned NOT NULL DEFAULT '0' COMMENT '操做類別',
  `order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的order_id',
  `memo` varchar(1024) NOT NULL DEFAULT '',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_order_id` (`order_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的訂單';

order_id記錄了訂單號,type和memo用來記錄下是那種類型的操做鎖定的訂單,memo用來記錄一下操做內容。這張表能完成分佈式鎖的主要緣由正是因爲把order_id設置爲了UNIQUE KEY,因此同一個訂單號只能插入一次。因而對鎖的競爭就交給了數據庫,處理同一個訂單號的交易中心把訂單號插入表中,數據庫保證了只有一個交易中心能插入成功,其餘交易中心都會插入失敗。lock和unlock的僞代碼也很是簡單:mysql

def lock :
    exec sql: insert into lockedOrder(type,order_id,memo) values (type,order_id,memo)
    if result == true :
        return true
    else :
        return false

def unlock :
    exec sql: delete from lockedOrder where order_id='order_id'

讀者能夠發現,這個鎖從功能上有幾個問題:redis

  • 數據庫鎖實現只能是非阻塞鎖,即應該爲tryLock,是嘗試得到鎖,若是沒法得到則會返回失敗。要改爲阻塞鎖,須要反覆執行insert語句直到插入成功。因爲交易中心的使用場景,只要一個交易中心處理訂單就好了,因此這裏不須要使用阻塞鎖。
  • 這把鎖沒有過時時間,若是交易中心鎖定了訂單,但異常宕機後,這個訂單就沒法鎖定了。這裏爲了讓鎖可以失效,須要在應用層加上定時任務,去刪除過時還未解鎖的訂單。clear_timeout_lock的僞代碼很簡單,只要執行一條sql便可。算法

    def clear_timeout_lock :
        exec sql : delete from lockedOrder where update_time <  ADDTIME(NOW(),'-00:02:00')

    這裏設置過時時間爲2分鐘,也是從業務場景考慮的,若是訂單處理時間可能超過2分鐘的話,這個時候還須要加大。sql

  • 這把鎖是不能重入的,意思就是即便一個交易中心得到了鎖,在它爲解鎖前,以後的流程若是有再去獲取鎖的話還會失敗,這樣就可能出現死鎖。這個問題咱們當時沒有處理,若是要處理這個問題的話,須要增長字段,在insert的時候,把該交易中心的標識加進來,這樣再獲取鎖的時候, 經過select,看下鎖定的人是否是本身。lock的僞代碼版本以下:數據庫

    def lock :
        exec sql: insert into lockedOrder(type,order_id,memo) values (type,order_id,memo)
        if result == true :
            return true
        else :
            exec sql : select id from lockedOrder where order_id='order_id' and memo = 'TradeCenterId'
            if count > 0 :
                return true
            else 
                return false

    在鎖定失敗後,看下鎖是否是本身,若是是本身,那依然鎖定成功。不過這個方法解鎖又遇到了困難,第一次unlock就把鎖給釋放了,後面的流程都是在沒鎖的狀況下完成,就可能出現其餘交易中心也獲取到這個訂單鎖,產生衝突。解決這個辦法的方法就是給鎖加計數器,記錄下lock多少次。unlock的時候,只有在lock次數爲0後才能刪除數據庫的記錄。apache

能夠看出,數據庫鎖能實現一個簡單的避免共享資源被多個系統操做的狀況。我之前在盛大的時候,發現盛大特別喜歡用數據庫鎖。盛大的前輩們會說,盛大基本上實現分佈式鎖用的都是數據庫鎖。在併發量不是那麼恐怖的狀況下,數據庫鎖的性能也不容易出問題,並且因爲數據庫的數據具備持久化的特性,通常的應用也足夠應付。可是除了上面說的數據庫鎖的幾個功能問題外,數據庫鎖並無很好的應付數據庫宕機的場景,若是數據庫宕機,會帶來的整個交易中心沒法工做。當時我也沒想過這個問題,咱們整個交易系統,數據庫是個單點,不過數據庫實在是太穩定了,兩年也沒出過任何問題。隨着工做經驗的積累,構建高可用系統的概念愈來愈強,系統中是不容許出現單點的。如今想一想,經過數據庫的同步複製,以及使用vip切換Master就能解決這個問題。編程

緩存鎖

後來我開始接觸緩存服務,知道不少應用都把緩存做爲分佈式鎖,好比redis。使用緩存做爲分佈式鎖,性能很是強勁,在一些不錯的硬件上,redis能夠每秒執行10w次,內網延遲不超過1ms,足夠知足絕大部分應用的鎖定需求。

redis鎖定的原理是利用setnx命令,即只有在某個key不存在狀況才能set成功該key,這樣就達到了多個進程併發去set同一個key,只有一個進程能set成功。

僅有一個setnx命令,redis遇到的問題跟數據庫鎖同樣,可是過時時間這一項,redis自帶的expire功能能夠不須要應用主動去刪除鎖。並且從 Redis 2.6.12 版本開始,redis的set命令直接直接設置NX和EX屬性,NX即附帶了setnx數據,key存在就沒法插入,EX是過時屬性,能夠設置過時時間。這樣一個命令就能原子的完成加鎖和設置過時時間。

緩存鎖優點是性能出色,劣勢就是因爲數據在內存中,一旦緩存服務宕機,鎖數據就丟失了。像redis自帶複製功能,能夠對數據可靠性有必定的保證,可是因爲複製也是異步完成的,所以依然可能出現master節點寫入鎖數據而未同步到slave節點的時候宕機,鎖數據丟失問題。

分佈式緩存鎖—Redlock

redis做者鑑於單點redis做爲分佈式鎖的可能出現的鎖數據丟失問題,提出了Redlock算法,該算法實現了比單一節點更安全、可靠的分佈式鎖管理(DLM)。下面我就介紹下Redlock的實現。

Redlock算法假設有N個redis節點,這些節點互相獨立,通常設置爲N=5,這N個節點運行在不一樣的機器上以保持物理層面的獨立。

算法的步驟以下:

  • 一、客戶端獲取當前時間,以毫秒爲單位。
  • 二、客戶端嘗試獲取N個節點的鎖,(每一個節點獲取鎖的方式和前面說的緩存鎖同樣),N個節點以相同的key和value獲取鎖。客戶端須要設置接口訪問超時,接口超時時間須要遠遠小於鎖超時時間,好比鎖自動釋放的時間是10s,那麼接口超時大概設置5-50ms。這樣能夠在有redis節點宕機後,訪問該節點時能儘快超時,而減少鎖的正常使用。
  • 三、客戶端計算在得到鎖的時候花費了多少時間,方法是用當前時間減去在步驟一獲取的時間,只有客戶端得到了超過3個節點的鎖,並且獲取鎖的時間小於鎖的超時時間,客戶端纔得到了分佈式鎖。
  • 四、客戶端獲取的鎖的時間爲設置的鎖超時時間減去步驟三計算出的獲取鎖花費時間。
  • 五、若是客戶端獲取鎖失敗了,客戶端會依次刪除全部的鎖。

使用Redlock算法,能夠保證在掛掉最多2個節點的時候,分佈式鎖服務仍然能工做,這相比以前的數據庫鎖和緩存鎖大大提升了可用性,因爲redis的高效性能,分佈式緩存鎖性能並不比數據庫鎖差。

分佈式專家質疑Redlock

介紹了Redlock,就能夠提及文章開頭提到了分佈式專家和redis做者的爭論了。

該專家提到,考慮分佈式鎖的時候須要考慮兩個方面:性能和正確性。

若是使用高性能的分佈式鎖,對正確性要求不高的場景下,那麼使用緩存鎖就足夠了。

若是使用可靠性高的分佈式鎖,那麼就須要考慮嚴格的可靠性問題。而Redlock則不符合正確性。爲何不符合呢?專家列舉了幾個方面。

如今不少編程語言使用的虛擬機都有GC功能,在Full GC的時候,程序會停下來處理GC,有些時候Full GC耗時很長,甚至程序有幾分鐘的卡頓,文章列舉了HBase的例子,HBase有時候GC幾分鐘,會致使租約超時。並且Full GC何時到來,程序沒法掌控,程序的任什麼時候候均可能停下來處理GC,好比下圖,客戶端1得到了鎖,正準備處理共享資源的時候,發生了Full GC直到鎖過時。這樣,客戶端2又得到了鎖,開始處理共享資源。在客戶端2處理的時候,客戶端1 Full GC完成,也開始處理共享資源,這樣就出現了2個客戶端都在處理共享資源的狀況。

Alt text

專家給出瞭解決辦法,以下圖,看起來就是MVCC,給鎖帶上token,token就是version的概念,每次操做鎖完成,token都會加1,在處理共享資源的時候帶上token,只有指定版本的token可以處理共享資源。

Alt text

而後專家還說到了算法依賴本地時間,並且redis在處理key過時的時候,依賴gettimeofday方法得到時間,而不是monotonic clock,這也會帶來時間的不許確。好比一下場景,兩個客戶端client 1和client 2,5個redis節點nodes (A, B, C, D and E)。

  • 一、client 1從A、B、C成功獲取鎖,從D、E獲取鎖網絡超時。
  • 二、節點C的時鐘不許確,致使鎖超時。
  • 三、client 2從C、D、E成功獲取鎖,從A、B獲取鎖網絡超時。
  • 四、這樣client 1和client 2都得到了鎖。

總結專家關於Redlock不可用的兩點:

  • 一、GC等場景可能隨時發生,並致使在客戶端獲取了鎖,在處理中超時,致使另外的客戶端獲取了鎖。專家還給出了使用自增token的解決方法。
  • 二、算法依賴本地時間,會出現時鐘不許,致使2個客戶端同時得到鎖的狀況。

因此專家給出的結論是,只有在有界的網絡延遲、有界的程序中斷、有界的時鐘錯誤範圍,Redlock才能正常工做,可是這三種場景的邊界又是沒法確認的,因此專家不建議使用Redlock。對於正確性要求高的場景,專家推薦了Zookeeper,關於使用Zookeeper做爲分佈式鎖後面再討論。

redis做者解疑Redlock

redis做者看到這個專家的文章後,寫了一篇博客予以迴應。做者很客氣的感謝了專家,而後表達出了對專家觀點的不認同。

I asked for an analysis in the original Redlock specification here: http://redis.io/topics/distlock. So thank you Martin. However I don’t agree with the analysis.

redis做者關於使用token解決鎖超時問題能夠歸納成下面五點:

  • 觀點1,使用分佈式鎖通常是在,你沒有其餘方式去控制共享資源了,專家使用token來保證對共享資源的處理,那麼就不須要分佈式鎖了。
  • 觀點2,對於token的生成,爲保證不一樣客戶端得到的token的可靠性,生成token的服務仍是須要分佈式鎖保證服務的可靠性。
  • 觀點3,對於專家說的自增的token的方式,redis做者認爲徹底不必,每一個客戶端能夠生成惟一的uuid做爲token,給共享資源設置爲只有該uuid的客戶端才能處理的狀態,這樣其餘客戶端就沒法處理該共享資源,直到得到鎖的客戶端釋放鎖。
  • 觀點四、redis做者認爲,對於token是有序的,並不能解決專家提出的GC問題,如上圖所示,若是token 34的客戶端寫入過程當中發送GC致使鎖超時,另外的客戶端可能得到token 35的鎖,並再次開始寫入,致使鎖衝突。因此token的有序並不能跟共享資源結合起來。
  • 觀點五、redis做者認爲,大部分場景下,分佈式鎖用來處理非事務場景下的更新問題。做者意思應該是有些場景很難結合token處理共享資源,因此得依賴鎖去鎖定資源並進行處理。

專家說到的另外一個時鐘問題,redis做者也給出瞭解釋。客戶端實際得到的鎖的時間是默認的超時時間,減去獲取鎖所花費的時間,若是獲取鎖花費時間過長致使超過了鎖的默認超時間,那麼此時客戶端並不能獲取到鎖,不會存在專家提出的例子。

再次分析Redlock

看了兩位專家你來我回的爭辯,相信讀者會對Redlock有了更多的認識。這裏我也想就分佈式專家提到的兩個問題結合redis做者的觀點,說說個人想法。

第一個問題我歸納爲,在一個客戶端獲取了分佈式鎖後,在客戶端的處理過程當中,可能出現鎖超時釋放的狀況,這裏說的處理中除了GC等非抗力外,程序流程未處理完也是可能發生的。以前在說到數據庫鎖設置的超時時間2分鐘,若是出現某個任務佔用某個訂單鎖超過2分鐘,那麼另外一個交易中心就能夠得到這把訂單鎖,從而兩個交易中心同時處理同一個訂單。正常狀況,任務固然秒級處理完成,但是有時候,加入某個rpc請求設置的超時時間過長,一個任務中有多個這樣的超時請求,那麼,極可能就出現超過自動解鎖時間了。當初咱們的交易模塊是用C++寫的,不存在GC,若是用java寫,中間還可能出現Full GC,那麼鎖超時解鎖後,本身客戶端沒法感知,是件很是嚴重的事情。我以爲這不是鎖自己的問題,上面說到的任何一個分佈式鎖,只要自帶了超時釋放的特性,都會出現這樣的問題。若是使用鎖的超時功能,那麼客戶端必定得設置獲取鎖超時後,採起相應的處理,而不是繼續處理共享資源。Redlock的算法,在客戶端獲取鎖後,會返回客戶端能佔用的鎖時間,客戶端必須處理該時間,讓任務在超過該時間後中止下來。

第二個問題,天然就是分佈式專家沒有理解Redlock。Redlock有個關鍵的特性是,獲取鎖的時間是鎖默認超時的總時間減去獲取鎖所花費的時間,這樣客戶端處理的時間就是一個相對時間,就跟本地時間無關了。

由此看來,Redlock的正確性是能獲得很好的保證的。仔細分析Redlock,相比於一個節點的redis,Redlock提供的最主要的特性是可靠性更高,這在有些場景下是很重要的特性。可是我以爲Redlock爲了實現可靠性,卻花費了過大的代價。

  • 首先必須部署5個節點才能讓Redlock的可靠性更強。
  • 而後須要請求5個節點才能獲取到鎖,經過Future的方式,先併發向5個節點請求,再一塊兒得到響應結果,能縮短響應時間,不過仍是比單節點redis鎖要耗費更多時間。
  • 而後因爲必須獲取到5個節點中的3個以上,因此可能出現獲取鎖衝突,即你們都得到了1-2把鎖,結果誰也不能獲取到鎖,這個問題,redis做者借鑑了raft算法的精髓,經過沖突後在隨機時間開始,能夠大大下降衝突時間,可是這問題並不能很好的避免,特別是在第一次獲取鎖的時候,因此獲取鎖的時間成本增長了。
  • 若是5個節點有2個宕機,此時鎖的可用性會極大下降,首先必須等待這兩個宕機節點的結果超時才能返回,另外只有3個節點,客戶端必須獲取到這所有3個節點的鎖才能擁有鎖,難度也加大了。
  • 若是出現網絡分區,那麼可能出現客戶端永遠也沒法獲取鎖的狀況。

分析了這麼多緣由,我以爲Redlock的問題,最關鍵的一點在於Redlock須要客戶端去保證寫入的一致性,後端5個節點徹底獨立,全部的客戶端都得操做這5個節點。若是5個節點有一個leader,客戶端只要從leader獲取鎖,其餘節點能同步leader的數據,這樣,分區、超時、衝突等問題都不會存在。因此爲了保證分佈式鎖的正確性,我以爲使用強一致性的分佈式協調服務能更好的解決問題。

更好的分佈式鎖—zookeeper

提到分佈式協調服務,天然就想到了zookeeper。zookeeper實現了相似paxos協議,是一個擁有多個節點分佈式協調服務。對zookeeper寫入請求會轉發到leader,leader寫入完成,並同步到其餘節點,直到全部節點都寫入完成,才返回客戶端寫入成功。

zookeeper還有幾個特質,讓它很是適合做爲分佈式鎖服務。

  • zookeeper支持watcher機制,這樣實現阻塞鎖,能夠watch鎖數據,等到數據被刪除,zookeeper會通知客戶端去從新競爭鎖。
  • zookeeper的數據能夠支持臨時節點的概念,即客戶端寫入的數據是臨時數據,在客戶端宕機後,臨時數據會被刪除,這樣就實現了鎖的異常釋放。使用這樣的方式,就不須要給鎖增長超時自動釋放的特性了。

zookeeper實現鎖的方式是客戶端一塊兒競爭寫某條數據,好比/path/lock,只有第一個客戶端能寫入成功,其餘的客戶端都會寫入失敗。寫入成功的客戶端就得到了鎖,寫入失敗的客戶端,註冊watch事件,等待鎖的釋放,從而繼續競爭該鎖。

若是要實現tryLock,那麼競爭失敗就直接返回false便可。

zookeeper實現的分佈式鎖簡單、明瞭,分佈式鎖的關鍵技術都由zookeeper負責實現了。能夠看下《從Paxos到Zookeeper:分佈式一致性原理與實踐》書裏貼出來的分佈式鎖實現步驟

Alt text

須要使用zookeeper的分佈式鎖功能,可使用curator-recipes庫。Curator是Netflix開源的一套ZooKeeper客戶端框架,curator-recipes庫裏面集成了不少zookeeper的應用場景,分佈式鎖的功能在org.apache.curator.framework.recipes.locks包裏面,《跟着實例學習ZooKeeper的用法: 分佈式鎖》文章裏面詳細的介紹了curator-recipes分佈式鎖的使用,想要使用分佈式鎖功能的朋友們不妨一試。

總結

文章寫到這裏,基本把我關於分佈式鎖的瞭解介紹了一遍。能夠實現分佈式鎖功能的,包括數據庫、緩存、分佈式協調服務等等。根據業務的場景、現狀以及已經依賴的服務,應用可使用不一樣分佈式鎖實現。文章介紹了redis做者和分佈式專家關於Redlock,雖然最終以爲Redlock並不像分佈式專家說的那樣缺少正確性,不過我我的以爲,若是須要最可靠的分佈式鎖,仍是使用zookeeper會更可靠些。curator-recipes庫封裝的分佈式鎖,java應用也能夠直接使用。並且若是開始依賴zookeeper,那麼zookeeper不只僅提供了分佈式鎖功能,選主、服務註冊與發現、保存元數據信息等功能都能依賴zookeeper,這讓zookeeper不會那麼閒置。

參考資料:

相關文章
相關標籤/搜索