譯自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 實現)算法
咱們將從三個特性的角度出發來設計RedLock模型:安全
故障切換(failover)實現方式的侷限性網絡
經過Redis爲某個資源加鎖的最簡單方式就是在一個Redis實例中使用過時特性(expire)建立一個key, 若是得到鎖的客戶端沒有釋放鎖,那麼在必定時間內這個Key將會自動刪除,避免死鎖。
這種作法在表面上看起來可行,但分佈式鎖做爲架構中的一個組件,爲了不Redis宕機引發鎖服務不可用, 咱們須要爲Redis實例(master)增長熱備(slave),若是master不可用則將slave提高爲master。
這種主從的配置方式存在必定的安全風險,因爲Redis的主從複製是異步進行的, 可能會發生多個客戶端同時持有一個鎖的現象。多線程
此類場景是很是典型的競態模型:架構
如何正確實現單實例的鎖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申請的鎖。
例如:
經過執行上面腳本的方式釋放鎖,Client的解鎖操做只會解鎖本身曾經加鎖的資源。
官方推薦通從 /dev/urandom/中取20個byte做爲隨機數或者採用更加簡單的方式, 例如使用RC4加密算法在/dev/urandom中獲得一個種子(Seed),而後生成一個僞隨機流。
也能夠用更簡單的使用時間戳+客戶端編號的方式生成隨機數,
這種方式的安全性較差一些,可是對於絕大多數的場景來講也已經足夠安全了。
PX 操做後面的參數表明的是這key的存活時間,稱做鎖過時時間。
經過上面的兩個操做,咱們能夠完成得到鎖和釋放鎖操做。若是這個系統不宕機,那麼單點的鎖服務已經足夠安全,接下來咱們開始把場景擴展到分佈式系統。
RedLock算法介紹
下面例子中的分佈式環境包含N個Redis Master節點,這些節點相互獨立,無需備份。這些節點儘量相互隔離的部署在不一樣的物理機或虛擬機上(故障隔離)。
節點數量暫定爲5個(在須要投票的集羣中,5個節點的配置是比較合理的最小配置方式)。得到鎖和釋放鎖的方式仍然採用以前介紹的方法。
一個Client想要得到一個鎖須要如下幾個操做:
RedLock能保證鎖同步嗎?
這個算法成立的一個條件是:即便集羣中沒有同步時鐘,各個進程的時間流逝速度也要大致一致,而且偏差與鎖存活時間相比是比較小的。實際應用中的計算機也能知足這個條件:各個計算機中間有幾毫秒的時鐘漂移(clock drift)。
失敗重試機制
若是一個Client沒法得到鎖,它將在一個隨機延時後開始重試。使用隨機延時的目的是爲了與其餘申請同一個鎖的Client錯開申請時間,減小腦裂(split brain)發生的可能性。
三個Client同時嘗試得到鎖,分別得到了2,2,1個實例中的鎖,三個鎖請求所有失敗。
一個client在所有Redis實例中完成的申請時間越短,發生腦裂的時間窗口越小。因此比較理想的作法是同時向N個Redis實例發出異步的SET請求。
當Client沒有在大多數Master中得到鎖時,當即釋放已經取得的鎖時很是必要的。(PS.當極端狀況發生時,好比得到了部分鎖之後,client發生網絡故障,沒法再釋放鎖資源。
那麼其餘client從新得到鎖的時間將是鎖的過時時間)。
不管Client認爲在指定的Master中有沒有得到鎖,都須要執行釋放鎖操做。
咱們將從不一樣的場景分析RedLock算法是否足夠安全。首先咱們假設一個client在大多數的Redis實例中取得了鎖,
那麼:
因而,最早被SET的鎖將在TTL-(T2-T1)-CLOCK_DIRFT後自動過時,其餘的鎖將在以後陸續過時。
因此能夠獲得結論:全部的key這段時間內是同時被鎖住的。
在這段時間內,一半以上的Redis實例中這個key都處在被鎖定狀態,其餘的客戶端沒法得到這個鎖。
分佈式鎖系統的可用性主要依靠如下三種機制
若是一直持續的發生網絡故障,那麼沒有客戶端能夠申請到鎖。分佈式鎖系統也將沒法提供服務直到網絡故障恢復爲止。
用戶使用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長時間不可用。
轉載:https://www.jianshu.com/p/fba7dd6dcef5