鎖,核心是協調各個使用方對公共資源使用的一種機制。當存在多個使用方互斥地使用某一個公共資源時,爲了不併行使用致使的修改結果不可控,須要在某個地方記錄一個標記,這個標記可以被全部使用方看到,當標記不存在時,能夠設置標記而且得到公共資源的使用權,其他使用者發現標記已經存在時,只能等待標記擁有方釋放後,再去嘗試設置標記。這個標記便可以理解爲鎖。redis
在單機多線程的環境下,因爲使用環境簡單和通訊可靠,鎖的可見性和原子性很容易能夠保證,因此使用系統提供的互斥鎖等方案,能夠簡單和可靠地實現鎖功能。到了分佈式的環境下,因爲公共資源和使用方之間的分離,以及使用方和使用方之間的分離,相互之間的通訊由線程間的內存通訊變爲網絡通訊。網絡通訊的時延和不可靠,加上分佈式環境中各類故障的常態化發生,致使實現一個可靠的分佈式鎖服務須要考慮更多更復雜的問題。算法
目前常見的分佈式鎖服務,能夠分爲如下兩大類:本文從上述兩大類常見的分佈式鎖服務實現方案入手,從分佈式鎖服務的各個核心問題(核心架構、鎖數據一致性、鎖服務可用性、死鎖預防機制、易用性、性能)展開,嘗試對比分析各個實現方案的優劣和特色。數據庫
基於分佈式緩存實現的鎖服務,思路最爲簡單和直觀。和單機環境的鎖同樣,咱們把鎖數據存放在分佈式環境中的一個惟一結點,全部須要獲取鎖的調用方,都去此結點訪問,從而實現對調用方的互斥,而存放鎖數據的結點,使用各種分佈式緩存產品充當。其核心架構以下(以Redis爲例):後端
圖1.基於分佈式緩存實現的鎖服務典型架構緩存
基於Redis官方的文檔,對於一個嘗試獲取鎖的操做,流程以下:安全
一、 向Redis結點發送命令:服務器
SET (key=Lock_Name, value=my_random_value) NX PX 30000其中:網絡
二、 若是命令返回成功,則表明獲取鎖成功,不然獲取鎖失敗。多線程
對於一個擁有鎖的客戶端,釋放鎖,其流程以下:架構
一、 向Redis結點發送命令:基於上述流程,因爲Redis結點是單點存在,因此在鎖過時時間以內且Redis結點不發生故障的狀況下,鎖的安全性(即互斥性)能夠獲得保證。可是仍然有以下幾個問題須要考慮:
一、 預防死鎖的必要性
考慮以下場景,一個客戶端獲取鎖成功,可是在釋放鎖以前崩潰了,此時實際上它已經放棄了對公共資源的操做權,可是卻沒有辦法請求解鎖,那麼它就會一直持有這個鎖,而其它客戶端永遠沒法得到鎖。所以,對於絕大部分場景,此類死鎖場景是應該獲得考慮和避免。
二、 引入鎖自動過時時間來預防死鎖帶來的問題
爲了預防死鎖,利用分佈式緩存的結點自動過時特性來按期刪除死鎖結點,看似能夠解決問題。可是其中隱藏的隱患是:實質上,鎖自動過時清理是釋放了一個不屬於本身的鎖。那麼幾乎必然的,會破壞鎖的互斥性,考慮以下場景:
也許有一個疑問,第五步中,客戶端1恢復回來後,能夠比較下目前已經持有鎖的時間,若是發現已經快過時,則放棄對共享資源的操做便可避免互斥性失效的問題。事實上,客戶端1的時間和Redis結點的時間自己就存在偏移的可能性,更極端一點,Redis上的時間還可能發生跳變或者比客戶端時間跑得更快,因此,嚴格來說,任何依賴兩個時間比較的互斥性算法,都存在潛在的隱患。
三、 解鎖操做的原子性
引入全局惟一的my_random_value,目的是想保證每次解鎖操做,必定是解鎖的本身加的鎖。因爲Redis沒有可以提供基於數據版本號來刪除Key的原子操做的特性,其Watch的CAS機制自己基於鏈接(有其餘的分佈式緩存產品可以支持這個特性)。所以解鎖須要兩步,先查鎖回來確認Value這把鎖是本身加的,而後再發起Del解鎖。因爲Get和Del操做的非原子性,那麼解鎖自己也會存在破壞互斥性的狀況,考慮以下場景:
四、 Redis結點故障後,主備切換的數據一致性
考慮Redis結點宕機,若是長時間沒法恢復,則致使鎖服務長時間不可用。爲了保證鎖服務的可用性,一般的方案是給這個Redis節點掛一個Slave,當Master節點不可用的時候,系統自動切到Slave上。可是因爲Redis的主從複製(replication)是異步的,這可能致使在宕機切換過程當中喪失鎖的安全性。考慮下面的時序:
設想下,若是要避免這種狀況,只有在寫數據的時候,就阻塞地把數據寫多份,所有寫成功才返回,這樣才能保證鎖的安全性(分佈式緩存的同步主從複製)。但這樣就能夠即保證數據一致性,又保證服務可用性了嗎?其實否則,在鎖數據寫Master和Slave兩份,都寫成功才認爲加鎖成功的狀況下,若是Master寫成功,Slave寫超時(其實寫成功了),這個時候認爲加鎖是失敗的,可是主和備的數據產生了不一致,並且Slave自身穩定性以及Master和Slave的通訊穩定性還成爲了致使服務不可用的額外因素。因此基於分佈式緩存實現的鎖服務,要想解決分佈式系統一致性和可用性的核心問題,並非簡單的主從同步能夠搞定(核心仍是要靠Paxos這樣的分佈式一致性協議)。
一、鎖服務性能
因爲鎖數據基於Redis等分佈式緩存保存,基於內存的數據操做特性使得這類鎖服務擁有着很是好的性能表現。同時鎖服務調用方和鎖服務自己只有一次RTT就能夠完成交互,使得加鎖延遲也很低。因此,高性能、低延遲是基於分佈式緩存實現鎖服務的一大優點。所以,在對性能要求較高,可是能夠容忍極端狀況下丟失鎖數據安全性的場景下,很是適用。
二、數據一致性和可用性
鎖數據一致性基於上述的分析,基於分佈式緩存的鎖服務受限於通用分佈式緩存的定位,沒法徹底保證鎖數據的安全性,核心的問題能夠歸納爲三點:
基於分佈式緩存實現鎖服務,在業界還存在各種變種的方案,其核心是利用不一樣分佈式緩存產品的額外特性,來改善基礎方案的各種缺點,各種變種方案能提供的安全性和可用性也不盡相同。此處介紹一種業界最出名,同時也是引發過最大爭論的一個鎖服務變種方案-RedLock。
RedLock由Redis的做者Antirez提出,算是Redis官方對於實現分佈式鎖的指導規範。Redlock的算法描述就放在Redis的官網上(https://redis.io/topics/distlock)。
選擇對比分析RedLock,第一是由於它做爲Redis官方的鎖服務指導規範,在提出的時候業內也對其進行過不少爭議和討論;第二是RedLock的算法中,已經有了分佈式一致性算法中最核心的概念-多數派的思想。所以咱們在衆多變種中選擇RedLock來進行介紹和分析。
圖2.RedLock鎖服務流程圖
對於一個客戶端,依次執行下面各個步驟,來完成獲取鎖的操做:
RedLock算法的最核心也是最有價值之處,是引入了多數派思想,來解決單點故障對數據安全性和服務可用性的影響。因爲加鎖成功須要全部Redis結點中的多數結點贊成,所以只要集羣中結點有一半可以提供服務時,服務的可用性就可以保證。同時對於數據的一致性,只要對於一把鎖,其多數派結點的數據不丟,那麼鎖就不可能被另外的調用方同時得到(不夠多數派),因此鎖的安全性也能夠獲得保證。因此從核心算法來講,多數派的思想是對數據一致性的保證下,向保證服務可用性又進了一大步。
可是,多數派僅僅是算法最核心的理論保證。要實現一個工程上徹底保證鎖數據安全性,同時高可用的鎖服務,RedLock還有很遠的距離,這也是RedLock在業界引發不少爭議的地方,核心的問題見下面的分析。
一、 RedLock的安全性依舊強依賴於系統時間
在以前單點Redis鎖服務的時候已經分析過,因爲爲了預防死鎖,使用了過時自動刪除鎖的機制,因此致使安全性依賴於單機Redis上的時間服務不能異常,從而存在隱患(本質是違反了鎖持有者才能刪除鎖的原則)。一樣的,到了RedLock中,仍然有此問題,考慮以下的時序:假設一共有5個Redis節點:A, B, C, D, E。
因此一個安全的算法,是不該該依賴於系統時間的。消息可能在網絡中延遲任意長的時間,甚至丟失,系統時鐘也可能以任意方式出錯。一個好的分佈式算法,這些因素不該該影響它的安全性,只可能影響到它的有效性,也就是說,即便在很是極端的狀況下(好比系統時鐘嚴重錯誤),算法頂可能是不能在有限的時間內給出結果而已,而不該該給出錯誤的結果。
二、 缺少鎖數據丟失的識別機制和恢復機制
假設一共有5個Redis節點:A, B, C, D, E。見以下的事件序列:
此類問題的本質,是做爲多數派數據的一個結點,數據丟失以後(好比故障未落地、超時被清理等等),首先沒有可以區分丟失了哪些數據的能力,其次尚未恢復丟失數據的能力。這兩種能力都缺少的狀況下,數據結點就繼續正常地參與投票,從而致使的數據一致性被破壞。
RedLock也意識到了這個問題,因此在其中有一個延遲重啓(delayed restarts)的概念。也就是說,一個節點崩潰後,先不當即重啓它,而是等待一段時間再重啓,這段時間應該大於其上全部鎖的有效時間的最大值。這樣的話,這個節點在重啓前所參與的鎖都會過時,它在重啓後就不會對現有的鎖形成影響。這個方案,是在缺少丟失數據識別的能力下,實現的較「悲觀」的一個替代方案,首先其方案依舊依賴於時間,其次如何肯定最大過時時間,也是一個麻煩的事情,由於最大過時時間極可能也一塊兒丟失了(未持久化),再有延遲重啓使得故障結點恢復的時間延長,增長了集羣服務可用性的隱患。怎麼來看,都不算一個優雅的方案。
一、鎖服務性能
因爲RedLock鎖數據仍然基於Redis保存,因此和基於單點的Redis鎖同樣,具備高性能和低延遲的特性,不過因爲引入多數派的思想,加鎖和解鎖時的併發寫,因此在流量消耗來講,比基於單點的Redis鎖消耗要大。從資源角度來講,是用流量換取了比單點Redis稍高的數據一致性和服務可用性。
二、數據一致性和可用性
RedLock的核心價值,在於多數派思想。不過根據上面的分析,它依然不是一個工程上能夠徹底保證鎖數據一致性的鎖服務。相比於基於單點Redis的鎖服務,RedLock解決了鎖數據寫入時多份的問題,從而能夠克服單點故障下的數據一致性問題,可是仍是受限於通用存儲的定位,其鎖服務總體機制上的不完備,使得沒法徹底保證鎖數據的安全性。在繼承自基於單點的Redis鎖服務缺陷(解鎖不具有原子性;鎖服務、調用方、資源方缺少確認機制)的基礎上,其核心的問題爲:缺少鎖數據丟失的識別和學習機制。
RedLock中的每臺Redis,充當的仍舊只是存儲鎖數據的功能,每臺Redis之間各自獨立,單臺Redis缺少全局的信息,天然也不知道本身的鎖數據是不是完整的。在單臺Redis數據的不完整的前提下,沒有識別和學習機制,使得在各類分佈式環境的典型場景下(結點故障、網絡丟包、網絡亂序),沒有完整數據但參與決策,從而破壞數據一致性。
分析完上述的鎖服務方案,能夠看到,各類方案核心仍是在一致性和可用性之間作取捨。對於鎖服務自己的定位和用途而言,其是一個相對中心化,對數據一致性有嚴格要求的場景。因此在分佈式環境下,把數據嚴格一致性做爲第一要求的狀況下,Paxos算法是繞不開的一個算法(「all working protocols for asynchronous consensus we have so far encountered have Paxos at their core」)。因而就有了Chubby和Zookeeper這類,基於分佈式一致性算法(核心是Paxos和相關變種)實現的鎖服務。
Chubby是由Google開發實現,在其內部使用的一個分佈式鎖服務,其核心設計Google經過論文的形式開源出來。而Zookeeper做爲Chubby的開源實現版本,由開源社區開發,目前也普遍應用在各類場景下。因爲Zookeeper和Chubby之間的關係,二者在絕大部分的設計上都十分類似,所以此部分以Chubby爲例,來分析此類鎖服務的相關特色,關於Zookeeper和Chubby設計上的差別,在本節最後簡要分析。
圖3.Chubby的系統結構
如上圖,一個典型的Chubby集羣,或者叫Chubby Cell,一般由5臺服務器(奇數臺)組成。這些服務器之間採用Paxos協議,經過投票方式決定一個服務器做爲Master。一旦一個服務器成爲Master,Chubby會保證一段時間其餘服務器不會成爲Master,這段時間被稱爲租期。在運行過程當中,Master服務器會不斷續租,若是Master服務器發生故障,餘下的服務器會選舉新的Master產生新的Master服務器。
Chubby客戶端經過DNS發現Chubby集羣的地址,而後Chubby客戶端會向Chubby集羣詢問Master服務器IP,在詢問過程,那些非Master服務器會將Master服務器標識反饋給客戶端,能夠很是快的定位到Master。
在實際運行中,全部的讀寫請求都發給Master。針對寫請求,Chubby Master會採用一致性協議將其廣播到全部副本服務器,而且在過半機器接受請求後,再響應客戶端。對於讀請求,Master服務器直接處理返回。
Chubby的一致性協議是Paxos算法的工程實現,對於Paxos協議自己,因爲不是此文的重點,因此此處不展開詳細介紹。整體上,能夠理解爲Chubby的一致性協議,能夠保證經過Master寫成功以後的數據,最終會擴散到集羣內的全部機器上,同時對於屢次的寫操做,Chubby能夠嚴格保證時序(不管是Master掛掉從新選舉產生新Master,仍是其中非Master機器的故障或者是被替換),另外從Master讀取的數據也是最新的數據。而知足這一切要求的前提,只須要Chubby集羣中的大部分機器能夠正常提供服務便可。
每臺Chubby服務器的基本架構大體分三層:
圖4.Chubby結點的基本架構
Chubby做爲分佈式鎖服務,提供的數據操做接口是相似於Unix文件系統接口風格的接口,這樣設計的初衷聽說是文件系統操做風格的接口在Google內部更加符合使用者的習慣。Chubby中全部的數據都是以文件結點的形式提供給調用者訪問,Chubby中典型的結點以下:/ls/foo/wombat/pouch。
結點分爲永久結點和臨時結點,臨時結點在沒有客戶端打開或者其子目錄下已經爲空的狀況下自動刪除。每一個結點均可以設置訪問控制權限(ACL),同時結點的原數據(MeteData)中有四個遞增的64位數,用於區分結點在各個方面的修改時序:一、實體編號:區分同名結點的前後;二、文件內容編號:文件內容修改時自增;三、鎖編號:鎖結點被獲取時自增;四、ACL編號:ACL變化時自增。
基於文件結點的組織形式,Chubby提供的數據操做API以下:
爲了不大量客戶端輪詢服務器帶來的壓力,Chubby提供了事件通知機制。Chubby客戶端能夠向Chubby註冊事件通知,當觸發了這些事件後服務端就會向客戶端發送事件通知。Chubby支持的事件類型包括不限於:
結合上述Chubby的設計細節,Chubby中客戶端完成加鎖的操做序列以下:
Chubby的加鎖流程看起來十分簡單,咱們來詳細分析下,Chubby如何解決以前幾種方案碰到的問題:
所以,Chubby經過一致性協議,解決了單點Redis數據沒有多份的問題,同時解決了RedLock沒法識別缺失數據和學習缺失數據的問題。在可用性方面,只要集羣大部分機器正常工做,Chubby就能保持正常對外提供服務。在數據一致性和可用性方面,Chubby這類方案明顯優於前兩種方案(這自己就是Paxos協議的長處)。
總結起來,Chubby引入了資源方和鎖服務的驗證,來避免了鎖服務自己孤立地作預防死鎖機制而致使的破壞鎖安全性的風險。同時依靠Session來維持鎖的持有狀態,在正常狀況下,客戶端能夠持有鎖任意長的時間,這能夠確保它作完全部須要的資源訪問操做以後再釋放鎖。這避免了基於Redis的鎖對於有效時間(lock validity time)到底設置多長的兩難問題。
不過引入的代價是資源方須要做對應修改,對於資源方不方便做修改的場景,Chubby提供了一種替代的機制Lock-Delay,來儘可能避免因爲預防死鎖而致使的鎖安全性被破壞。Chubby容許客戶端爲持有的鎖指定一個Lock-Delay的時間值(默認是1分鐘)。當Chubby發現客戶端被動失去聯繫的時候,並不會當即釋放鎖,而是會在Lock-Delay指定的時間內阻止其它客戶端得到這個鎖。這是爲了在把鎖分配給新的客戶端以前,讓以前持有鎖的客戶端有充分的時間把請求隊列排空(draining the queue),儘可能防止出現延遲到達的未處理請求。
可見,爲了應對鎖失效問題,Chubby提供的兩種處理方式:CheckSequencer()檢查和Lock-Delay,它們對於安全性的保證是從強到弱的。可是Chubby確實提供了單調遞增的鎖序號,以及資源服務器和Chubby的溝通渠道,這就容許資源服務器在須要的時候,利用它提供更強的安全性保障。
一、 對於服務讀負載的取捨
Chubby設計爲全部的讀寫都通過Master處理,這必然致使Master的負載太高,所以Chubby在Client端實現了緩存機制。Client端在本地有文件內容的Cache,Client端對Cache的維護只是負責讓Cache失效,而不持續更新Cache,失效後的Cache,在Client下一次訪問Master以後從新建立。每次修改後,Master經過Client的保活包(因此保活包除了有延長Session租約和通知事件的功能外,還有一個功能是Cacha失效),通知每一個擁有此Cache的Client(Master維護了每一個Client可能擁有的Cache信息),讓他們的Cache失效,Client收到保活包以後,刪除本地Cache。若是Client未收到本次保活包,那麼只有兩種可能,後續的保活包學習到Cache失效的內容,或者Session超時,清空全部Cache從新創建Session。因此對Cache機制而言,不可以保證Cache數據的隨時最新,可是能夠保證最終的Cache數據一致性,同時能夠大量避免每次向Master讀帶來的網絡流量開銷和Master的高負載。
Zookeeper設計採起了另一個思路,其中Client能夠鏈接集羣中任意一個節點,而不是必需要鏈接Master。Client的全部寫請求必須轉給Master處理,而讀請求,能夠由普通結點直接處理返回,從而分擔了Master的負載。一樣的,讀數據不能保證時刻的一致性,可是能夠保證最終一致性。
二、 預防死鎖方面
Chubby提供了CheckSequencer()檢查和Lock-Delay兩種方式來避免鎖失效帶來的問題,引入了資源方和鎖服務方的交互來保證鎖數據安全性。不過Zookeeper目前尚未相似於CheckSequencer的機制,而只有相似於Lock-Delay的等待機制來儘可能避免鎖失效帶來的安全性問題。因此在鎖失效方面的安全性來講,Chubby提供了更好的保證。
三、 鎖使用便利性方面的差別
Chubby和Zookeeper都提供了事件機制,這個機制能夠這樣來使用,好比當客戶端試圖建立/lock的時候,發現它已經存在了,這時候建立失敗,但客戶端不必定就此對外宣告獲取鎖失敗。客戶端能夠進入一種等待狀態,等待當/lock節點被釋放的時候,鎖服務經過事件機制通知它,這樣它就能夠繼續完成建立操做(獲取鎖)。這可讓分佈式鎖在客戶端用起來就像一個本地的鎖同樣:加鎖失敗就阻塞住,直到獲取到鎖爲止。
可是考慮這樣一個問題,當有大量的客戶端都阻塞在/lock結點上時,一旦以前的持有者釋放鎖,那麼阻塞的潛在調用方都會被激活,可是大量客戶端被激活,從新發起加鎖操做時,又只有一個客戶端能成功,形成所謂的「驚羣」效應。
考慮到這一點,Zookeeper上實現了一個「有序臨時結點」的功能,來避免驚羣。對於一個臨時鎖結點,Zookeeper支持每次建立均可以成功,可是每次建立的結點經過一個自增的序號來區別。建立成功最小結點的客戶端代表得到了鎖,而其餘調用方建立的結點序號代表其處於鎖等待隊列中的位置。因此,對於獲取鎖失敗的客戶端,其只須要監聽序號比其小的最大結點的釋放狀況,就能夠判斷什麼時候本身有機會競爭鎖。而不是每次一旦有鎖釋放,都去嘗試從新加鎖,從而避免「驚羣」效應產生。
本文經過分析三類分佈式鎖服務,基本涵蓋了全部分佈式鎖服務中涉及到的關鍵技術,以及對應具體的工程實現方案。
基於分佈式存儲實現的鎖服務,因爲其內存數據存儲的特性,因此具備結構簡單,高性能和低延遲的優勢。可是受限於通用存儲的定位,其在鎖數據一致性上缺少嚴格保證,同時
其在解鎖驗證、故障切換、死鎖處理等方面,存在各類問題。因此其適用於在對性能要求較高,可是能夠容忍極端狀況下丟失鎖數據安全性的場景下。
基於分佈式一致性算法實現的鎖服務,其使用Paxos協議保證了鎖數據的嚴格一致性,同時又具有高可用性。在要求鎖數據嚴格一致的場景下,此類鎖服務幾乎是惟一的選擇。可是因爲其結構和分佈式一致性協議的複雜性,其在性能和加鎖延遲上,比基於分佈式存儲實現的鎖服務要遜色。
因此實際應用場景下,須要根據具體需求出發,權衡各類考慮因素,選擇合適的鎖服務實現模型。不管選擇哪種模型,須要咱們清楚地知道它在安全性上有哪些不足,以及它會帶來什麼後果。更特別的,若是是對鎖數據安全性要求十分嚴格的應用場景,那麼須要更加慎之又慎。在本文的討論中,咱們看到,在分佈式鎖的正確性上走得最遠的,是基於Paxos實現,同時引入分佈式資源進行驗證的方案。下一篇,咱們來介紹歡樂遊戲基於實際業務場景,結合三類方案各自的特色,實現的一個嚴格保證鎖數據安全性的高可用高性能鎖服務方案。