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

譯自Redis官方文檔php

在多線程共享臨界資源的場景下,分佈式鎖是一種很是重要的組件。
許多庫使用不一樣的方式使用redis實現一個分佈式鎖管理。
其中有一部分簡單的實現方式可靠性不足,能夠經過一些簡單的修改提升其可靠性。
這篇文章介紹了一種指導性的redis分佈式鎖算法RedLock,RedLock比起單實例的實現方式更加安全。node

在介紹RedLock算法以前,咱們列出了一些已經實現了分佈式鎖的類庫供你們參考。redis

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也持有這個鎖。

如何正確實現單實例的鎖dom

在單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

相關文章
相關標籤/搜索