1. 程式人生 > >RedLock演算法-使用redis實現分散式鎖服務

RedLock演算法-使用redis實現分散式鎖服務

譯自Redis官方文件

在多執行緒共享臨界資源的場景下,分散式鎖是一種非常重要的元件。
許多庫使用不同的方式使用redis實現一個分散式鎖管理。
其中有一部分簡單的實現方式可靠性不足,可以通過一些簡單的修改提高其可靠性。
這篇文章介紹了一種指導性的redis分散式鎖演算法RedLock,RedLock比起單例項的實現方式更加安全。

在介紹RedLock演算法之前,我們列出了一些已經實現了分散式鎖的類庫供大家參考。

Redlock-rb (Ruby 實現).
Redlock-py (Python 實現)
Redlock-php (PHP 實現)
PHPRedisMutex (further PHP 實現)??
Redsync.go (Go 實現)
Redisson (Java 實現)
Redis::DistLock (Perl 實現)
Redlock-cpp (C++ 實現)
Redlock-cs (C#/.NET 實現)
RedLock.net (C#/.NET 實現
ScarletLock (C# .NET 實現)
node-redlock (NodeJS 實現)

分散式鎖應該具有的特性(Safety & Liveness)

我們將從三個特性的角度出發來設計RedLock模型:

  1. 安全性(Safety):在任意時刻,只有一個客戶端可以獲得鎖(排他性)。
  2. 避免死鎖:客戶端最終一定可以獲得鎖,即使鎖住某個資源的客戶端在釋放鎖之前崩潰或者網路不可達。
  3. 容錯性:只要Redsi叢集中的大部分節點存活,client就可以進行加鎖解鎖操作。

故障切換(failover)實現方式的侷限性

通過Redis為某個資源加鎖的最簡單方式就是在一個Redis例項中使用過期特性(expire)建立一個key, 如果獲得鎖的客戶端沒有釋放鎖,那麼在一定時間內這個Key將會自動刪除,避免死鎖。
這種做法在表面上看起來可行,但分散式鎖作為架構中的一個元件,為了避免Redis宕機引起鎖服務不可用, 我們需要為Redis例項(master)增加熱備(slave),如果master不可用則將slave提升為master。
這種主從的配置方式存在一定的安全風險,由於Redis的主從複製是非同步

進行的, 可能會發生多個客戶端同時持有一個鎖的現象。

此類場景是非常典型的競態模型

  1. Client A 獲得在master節點獲得了鎖
  2. 在master將key備份到slave節點之前,master宕機
  3. slave 被提升為master
  4. Client B 在新的master節點處獲得了鎖,Client A也持有這個鎖。

如何正確實現單例項的鎖

在單redis例項中實現鎖是分散式鎖的基礎,在解決前文提到的單例項的不足之前,我們先了解如何在單點中正確的實現鎖。
如果你的應用可以容忍偶爾發生競態問題,那麼單例項鎖就足夠了。

我們通過以下命令對資源加鎖
SET resource_name my_random_value NX PX 30000


SET NX 命令只會在Key不存在的時給key賦值,PX 命令通知redis儲存這個key 30000ms。
my_random_value必須是全域性唯一的值。這個隨機數在釋放鎖時保證釋放鎖操作的安全性。

通過下面的指令碼為申請成功的鎖解鎖:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

如果key對應的Value一致,則刪除這個key。

通過這個方式釋放鎖是為了避免client釋放了其他client申請的鎖。
例如:

  1. Client A 獲得了一個鎖,
  2. 當嘗試釋放鎖的請求傳送給Redis時被阻塞,沒有及時到達Redis。
  3. 鎖定時間超時,Redis認為鎖的租約到期,釋放了這個鎖。
  4. client B 重新申請到了這個鎖
  5. client A的解鎖請求到達,將Client B鎖定的key解鎖
  6. Client C 也獲得了鎖
  7. Client B client C 同時持有鎖。

通過執行上面指令碼的方式釋放鎖,Client的解鎖操作只會解鎖自己曾經加鎖的資源。
官方推薦通從 /dev/urandom/中取20個byte作為隨機數或者採用更加簡單的方式, 例如使用RC4加密演算法在/dev/urandom中得到一個種子(Seed),然後生成一個偽隨機流。
也可以用更簡單的使用時間戳+客戶端編號的方式生成隨機數,
這種方式的安全性較差一些,但是對於絕大多數的場景來說也已經足夠安全了。

PX 操作後面的引數代表的是這key的存活時間,稱作鎖過期時間。

  1. 當資源被鎖定超過這個時間,鎖將自動釋放。
  2. 獲得鎖的客戶端如果沒有在這個時間視窗內完成操作,就可能會有其他客戶端獲得鎖,引起爭用問題。

通過上面的兩個操作,我們可以完成獲得鎖和釋放鎖操作。如果這個系統不宕機,那麼單點的鎖服務已經足夠安全,接下來我們開始把場景擴充套件到分散式系統。

RedLock演算法介紹

下面例子中的分散式環境包含N個Redis Master節點,這些節點相互獨立,無需備份。這些節點儘可能相互隔離的部署在不同的物理機或虛擬機器上(故障隔離)。
節點數量暫定為5個(在需要投票的叢集中,5個節點的配置是比較合理的最小配置方式)。獲得鎖和釋放鎖的方式仍然採用之前介紹的方法。

一個Client想要獲得一個鎖需要以下幾個操作:

  1. 得到本地時間
  2. Client使用相同的key和隨機數,按照順序在每個Master例項中嘗試獲得鎖。在獲得鎖的過程中,為每一個鎖操作設定一個快速失敗時間(如果想要獲得一個10秒的鎖, 那麼每一個鎖操作的失敗時間設為5-50ms)。
    這樣可以避免客戶端與一個已經故障的Master通訊佔用太長時間,通過快速失敗的方式儘快的與叢集中的其他節點完成鎖操作。
  3. 客戶端計算出與master獲得鎖操作過程中消耗的時間,當且僅當Client獲得鎖消耗的時間小於鎖的存活時間,並且在一半以上的master節點中獲得鎖。才認為client成功的獲得了鎖。
  4. 如果已經獲得了鎖,Client執行任務的時間視窗是鎖的存活時間減去獲得鎖消耗的時間。
  5. 如果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例項中取得了鎖,
那麼:

  1. 每個例項中的鎖的剩餘存活時間相等為TTL。
  2. 每個鎖請求到達各個Redis例項中的時間有差異。
  3. 第一個鎖成功請求最先在T1後返回,最後返回的請求在T2後返回。(T1,T2都小於最大失敗時間)
  4. 並且每個例項之間存在時鐘漂移CLOCK_DRIFT(Time Drift)。

於是,最先被SET的鎖將在TTL-(T2-T1)-CLOCK_DIRFT後自動過期,其他的鎖將在之後陸續過期。
所以可以得到結論:所有的key這段時間內是同時被鎖住的。
在這段時間內,一半以上的Redis例項中這個key都處在被鎖定狀態,其他的客戶端無法獲得這個鎖。

鎖的可用性分析(Liveness)

分散式鎖系統的可用性主要依靠以下三種機制

  1. 鎖的自動釋放(key expire),最終鎖將被釋放並且被再次申請。
  2. 客戶端在未申請到鎖以及申請到鎖並完成任務後都將進行釋放鎖的操作,所以大部分情況下都不需要等待到鎖的自動釋放期限,其他client即可重新申請到鎖。
  3. 假設一個Client在大多數Redis例項中申請鎖請求所成功花費的時間為Tac。那麼如果某個Client第一次沒有申請到鎖,需要重試之前,必須等待一段時間T。T需要遠大於Tac。 因為多個Client同時請求鎖資源,他們有可能都無法獲得一半以上的鎖,導致腦裂雙方均失敗。設定較久的重試時間是為了減少腦裂產生的概率。

如果一直持續的發生網路故障,那麼沒有客戶端可以申請到鎖。分散式鎖系統也將無法提供服務直到網路故障恢復為止。

效能,故障恢復與檔案同步

使用者使用redis作為鎖服務的主要優勢是效能。其效能的指標有兩個

  1. 加鎖和解鎖的延遲
  2. 每秒可以進行多少加鎖和解鎖操作

所以,在客戶端與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的方式進行持久化,情況會稍好一些。例如我們可以向某個例項傳送shutdownrestart命令。即使節點被關閉,EX設定的時間仍在計算,鎖的排他性仍能保證。

但當Redis發生電源瞬斷的情況又會遇到有新的問題出現。如果Redis配置中的進行磁碟持久化的時間是每分鐘進行,那麼會有部分key在重新啟動後丟失。
如果為了避免key的丟失,將持久化的設定改為Always,那麼效能將大幅度下降。

另一種解決方案是在這臺例項重新啟動後,令其在一定時間內不參與任何加鎖。在間隔了一整個鎖生命週期後,重新參與到鎖服務中。這樣可以保證所有在這臺例項宕機期間內的key都已經過期或被釋放。

延時重啟機制能夠保證Redis即使不使用任何持久化策略,仍能保證鎖的可靠性。但是這種策略可能會犧牲掉一部分可用性。
例如叢集中超過半數的例項都宕機了,那麼整個分散式鎖系統需要等待一整個鎖有效期的時間才能重新提供鎖服務。

使鎖演算法更加可靠:鎖續約

如果Client進行的工作耗時較短,那麼可以預設使用一個較小的鎖有效期,然後實現一個鎖續約機制。

當一個Client在工作計算到一半時發現鎖的剩餘有效期不足。可以向Redis例項傳送續約鎖的Lua指令碼。如果Client在一定的期限內(耗間與申請鎖的耗時接近)成功的續約了半數以上的例項,那麼續約鎖成功。

為了提高系統的可用性,每個Client申請鎖續約的次數需要有一個最大限制,避免其不斷續約造成該key長時間不可用。

轉載:https://www.jianshu.com/p/fba7dd6dcef5