1. 程式人生 > >聊一聊分布式鎖的設計

聊一聊分布式鎖的設計

src set 可用性 slave 共享資源 get 處理過程 res 指定

起因

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

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

數據庫鎖表

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

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的偽代碼也非常簡單:

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‘

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

  • 數據庫鎖實現只能是非阻塞鎖,即應該為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分鐘的話,這個時候還需要加大。

  • 這把鎖是不能重入的,意思就是即使一個交易中心獲得了鎖,在它為解鎖前,之後的流程如果有再去獲取鎖的話還會失敗,這樣就可能出現死鎖。這個問題我們當時沒有處理,如果要處理這個問題的話,需要增加字段,在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後才能刪除數據庫的記錄。

可以看出,數據庫鎖能實現一個簡單的避免共享資源被多個系統操作的情況。我以前在盛大的時候,發現盛大特別喜歡用數據庫鎖。盛大的前輩們會說,盛大基本上實現分布式鎖用的都是數據庫鎖。在並發量不是那麽恐怖的情況下,數據庫鎖的性能也不容易出問題,而且由於數據庫的數據具有持久化的特性,一般的應用也足夠應付。但是除了上面說的數據庫鎖的幾個功能問題外,數據庫鎖並沒有很好的應付數據庫宕機的場景,如果數據庫宕機,會帶來的整個交易中心無法工作。當時我也沒想過這個問題,我們整個交易系統,數據庫是個單點,不過數據庫實在是太穩定了,兩年也沒出過任何問題。隨著工作經驗的積累,構建高可用系統的概念越來越強,系統中是不允許出現單點的。現在想想,通過數據庫的同步復制,以及使用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個節點運行在不同的機器上以保持物理層面的獨立。

算法的步驟如下:

  • 1、客戶端獲取當前時間,以毫秒為單位。
  • 2、客戶端嘗試獲取N個節點的鎖,(每個節點獲取鎖的方式和前面說的緩存鎖一樣),N個節點以相同的key和value獲取鎖。客戶端需要設置接口訪問超時,接口超時時間需要遠遠小於鎖超時時間,比如鎖自動釋放的時間是10s,那麽接口超時大概設置5-50ms。這樣可以在有redis節點宕機後,訪問該節點時能盡快超時,而減小鎖的正常使用。
  • 3、客戶端計算在獲得鎖的時候花費了多少時間,方法是用當前時間減去在步驟一獲取的時間,只有客戶端獲得了超過3個節點的鎖,而且獲取鎖的時間小於鎖的超時時間,客戶端才獲得了分布式鎖。
  • 4、客戶端獲取的鎖的時間為設置的鎖超時時間減去步驟三計算出的獲取鎖花費時間。
  • 5、如果客戶端獲取鎖失敗了,客戶端會依次刪除所有的鎖。

使用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個客戶端都在處理共享資源的情況。

技術分享

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

技術分享

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

  • 1、client 1從A、B、C成功獲取鎖,從D、E獲取鎖網絡超時。
  • 2、節點C的時鐘不準確,導致鎖超時。
  • 3、client 2從C、D、E成功獲取鎖,從A、B獲取鎖網絡超時。
  • 4、這樣client 1和client 2都獲得了鎖。

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

  • 1、GC等場景可能隨時發生,並導致在客戶端獲取了鎖,在處理中超時,導致另外的客戶端獲取了鎖。專家還給出了使用自增token的解決方法。
  • 2、算法依賴本地時間,會出現時鐘不準,導致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的客戶端才能處理的狀態,這樣其他客戶端就無法處理該共享資源,直到獲得鎖的客戶端釋放鎖。
  • 觀點4、redis作者認為,對於token是有序的,並不能解決專家提出的GC問題,如上圖所示,如果token 34的客戶端寫入過程中發送GC導致鎖超時,另外的客戶端可能獲得token 35的鎖,並再次開始寫入,導致鎖沖突。所以token的有序並不能跟共享資源結合起來。
  • 觀點5、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:分布式一致性原理與實踐》書裏貼出來的分布式鎖實現步驟

技術分享

需要使用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不會那麽閑置。

聊一聊分布式鎖的設計