redis常用的方式有單節點、主從模式、哨兵模式、集群模式。
單節點在生產環境基本上不會使用,因為不能達到高可用,且連RDB或AOF備份都只能放在master上,所以基本上不會使用。
另外幾種模式都無法避免兩個問題:
1、異步數據丟失。
2、腦裂問題。
所以redis官方針對這種情況提出了紅鎖(Redlock)的概念。
假設有5個redis節點,這些節點之間既沒有主從,也沒有集群關系。客戶端用相同的key和隨機值在5個節點上請求鎖,請求鎖的超時時間應小於鎖自動釋放時間。當在3個(超過半數)redis上請求到鎖的時候,才算是真正獲取到了鎖。如果沒有獲取到鎖,則把部分已鎖的redis釋放掉。
RedLock算法介紹
下面例子中的分布式環境包含N個Redis Master節點,這些節點相互獨立,無需備份。這些節點盡可能相互隔離的部署在不同的物理機或虛擬機上(故障隔離)。
節點數量暫定為5個(在需要投票的集群中,5個節點的配置是比較合理的最小配置方式)。獲得鎖和釋放鎖的方式仍然采用之前介紹的方法。
一個Client想要獲得一個鎖需要以下幾個操作:
得到本地時間
Client使用相同的key和隨機數,按照順序在每個Master實例中嘗試獲得鎖。在獲得鎖的過程中,為每一個鎖操作設置一個快速失敗時間(如果想要獲得一個10秒的鎖, 那么每一個鎖操作的失敗時間設為5-50ms)。
這樣可以避免客戶端與一個已經故障的Master通信占用太長時間,通過快速失敗的方式盡快的與集群中的其他節點完成鎖操作。
客戶端計算出與master獲得鎖操作過程中消耗的時間,當且僅當Client獲得鎖消耗的時間小於鎖的存活時間,並且在一半以上的master節點中獲得鎖。才認為client成功的獲得了鎖。
如果已經獲得了鎖,Client執行任務的時間窗口是鎖的存活時間減去獲得鎖消耗的時間。
如果Client獲得鎖的數量不足一半以上,或獲得鎖的時間超時,那么認為獲得鎖失敗。客戶端需要嘗試在所有的master節點中釋放鎖, 即使在第二步中沒有成功獲得該Master節點中的鎖,仍要進行釋放操作。
RedLock能保證鎖同步嗎?
這個算法成立的一個條件是:即使集群中沒有同步時鍾,各個進程的時間流逝速度也要大體一致,並且誤差與鎖存活時間相比是比較小的。實際應用中的計算機也能滿足這個條件:各個計算機中間有幾毫秒的時鍾漂移(clock drift)。
失敗重試機制
如果一個Client無法獲得鎖,它將在一個隨機延時后開始重試。使用隨機延時的目的是為了與其他申請同一個鎖的Client錯開申請時間,減少腦裂(split brain)發生的可能性。
三個Client同時嘗試獲得鎖,分別獲得了2,2,1個實例中的鎖,三個鎖請求全部失敗。
一個client在全部Redis實例中完成的申請時間越短,發生腦裂的時間窗口越小。所以比較理想的做法是同時向N個Redis實例發出異步的SET請求。
當Client沒有在大多數Master中獲得鎖時,立即釋放已經取得的鎖時非常必要的。(PS.當極端情況發生時,比如獲得了部分鎖以后,client發生網絡故障,無法再釋放鎖資源。
那么其他client重新獲得鎖的時間將是鎖的過期時間)。
無論Client認為在指定的Master中有沒有獲得鎖,都需要執行釋放鎖操作。
RedLock算法安全性分析
我們將從不同的場景分析RedLock算法是否足夠安全。首先我們假設一個client在大多數的Redis實例中取得了鎖,
那么:
每個實例中的鎖的剩余存活時間相等為TTL。
每個鎖請求到達各個Redis實例中的時間有差異。
第一個鎖成功請求最先在T1后返回,最后返回的請求在T2后返回。(T1,T2都小於最大失敗時間)
並且每個實例之間存在時鍾漂移CLOCK_DRIFT(Time Drift)。
於是,最先被SET的鎖將在TTL-(T2-T1)-CLOCK_DIRFT后自動過期,其他的鎖將在之后陸續過期。
所以可以得到結論:所有的key這段時間內是同時被鎖住的。
在這段時間內,一半以上的Redis實例中這個key都處在被鎖定狀態,其他的客戶端無法獲得這個鎖。
鎖的可用性分析(Liveness)
分布式鎖系統的可用性主要依靠以下三種機制
鎖的自動釋放(key expire),最終鎖將被釋放並且被再次申請。
客戶端在未申請到鎖以及申請到鎖並完成任務后都將進行釋放鎖的操作,所以大部分情況下都不需要等待到鎖的自動釋放期限,其他client即可重新申請到鎖。
假設一個Client在大多數Redis實例中申請鎖請求所成功花費的時間為Tac。那么如果某個Client第一次沒有申請到鎖,需要重試之前,必須等待一段時間T。T需要遠大於Tac。 因為多個Client同時請求鎖資源,他們有可能都無法獲得一半以上的鎖,導致腦裂雙方均失敗。設置較久的重試時間是為了減少腦裂產生的概率。
如果一直持續的發生網絡故障,那么沒有客戶端可以申請到鎖。分布式鎖系統也將無法提供服務直到網絡故障恢復為止。
性能,故障恢復與文件同步
用戶使用redis作為鎖服務的主要優勢是性能。其性能的指標有兩個
加鎖和解鎖的延遲
每秒可以進行多少加鎖和解鎖操作
所以,在客戶端與N個Redis節點通信時,必須使用多路發送的方式(multiplex),減少通信延時。
為了實現故障恢復還需要考慮數據持久化的問題。
我們還是從某個特定的場景分析:
<code>
Redis實例的配置不進行任何持久化,集群中5個實例 M1,M2,M3,M4,M5
client A獲得了M1,M2,M3實例的鎖。
此時M1宕機並重啟。
由於沒有進行持久化,M1重啟后不存在任何KEY
client B獲得M4,M5和重啟后的M1中的鎖。
此時client A 和Client B 同時獲得鎖
</code>
如果使用AOF的方式進行持久化,情況會稍好一些。例如我們可以向某個實例發送shutdown和restart命令。即使節點被關閉,EX設置的時間仍在計算,鎖的排他性仍能保證。
但當Redis發生電源瞬斷的情況又會遇到有新的問題出現。如果Redis配置中的進行磁盤持久化的時間是每分鍾進行,那么會有部分key在重新啟動后丟失。
如果為了避免key的丟失,將持久化的設置改為Always,那么性能將大幅度下降。
另一種解決方案是在這台實例重新啟動后,令其在一定時間內不參與任何加鎖。在間隔了一整個鎖生命周期后,重新參與到鎖服務中。這樣可以保證所有在這台實例宕機期間內的key都已經過期或被釋放。
延時重啟機制能夠保證Redis即使不使用任何持久化策略,仍能保證鎖的可靠性。但是這種策略可能會犧牲掉一部分可用性。
例如集群中超過半數的實例都宕機了,那么整個分布式鎖系統需要等待一整個鎖有效期的時間才能重新提供鎖服務。
使鎖算法更加可靠:鎖續
如果Client進行的工作耗時較短,那么可以默認使用一個較小的鎖有效期,然后實現一個鎖續約機制。
當一個Client在工作計算到一半時發現鎖的剩余有效期不足。可以向Redis實例發送續約鎖的Lua腳本。如果Client在一定的期限內(耗間與申請鎖的耗時接近)成功的續約了半數以上的實例,那么續約鎖成功。
為了提高系統的可用性,每個Client申請鎖續約的次數需要有一個最大限制,避免其不斷續約造成該key長時間不可用。