[翻譯]基於redis的分佈式鎖

本篇翻譯自【redis.io/topics/dist…php

在不少不一樣進程必須以相互排斥的方式競爭分片資源的狀況下,分佈式鎖是很是有用的原始功能。node

有不少的實現和博客都描述瞭如何基於Redis來實現分佈式鎖管理器(DLM,Distributed Lock Manager)。有的使用了不一樣的途徑,可是大多都是使用相同的簡單方案,與複雜的設計相比,下面這種官方的方案用更低的保證度來實現分佈式鎖。官方把它稱做是更加規範的分佈式鎖的實現方案,也就是所謂的RedLock。redis

實現

如今已經有不少的基於Redis的鎖實現,好比:算法

  • Redlock-rb (Ruby).
  • Redlock-py (Python).
  • Aioredlock (Asyncio Python).
  • Redlock-php (PHP).
  • PHPRedisMutex (further PHP)
  • cheprasov/php-redis-lock (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設計有三個原則,這三個原則在RedLock的設計者看來是有效的分佈式鎖的最低要求。數據庫

  • 安全屬性:保證互斥,任什麼時候候都只有一個客戶端持有鎖
  • 效率屬性1:不會死鎖。即便鎖定資源的客戶端崩潰或者被隔離分區,也要可以得到鎖。
  • 效率屬性2:容錯。只要大多數節點還在運行,那麼客戶端就能繼續得到和釋放鎖

基於故障轉移(failover-based)的方案是不夠的

想理解官方的分佈式鎖方案原理,就得了解現有的分佈式鎖的實現方式。安全

最簡單的方式是在單實例中使用Redis鎖住一個建立的key,這個key一般是有存活時間限制的,使用的是redis的expires特性,因此這種鎖最終都會釋放(知足效率屬性2)。當客戶端須要釋放資源,就刪除這個key。服務器

很容易理解的方案,可是有個問題。這是單點架構,若是Redis的master節點掛了,會發生什麼呢?固然你可能會使用添加slave的方法來解決這個問題。可是這是無效的,由於這種狀況下沒法保證互斥。緣由是Redis的副本複製是異步的。網絡

這個模型有明顯的競爭條件:架構

  • 1.客戶端A的鎖在master中
  • 2.在master向slave同步傳輸數據的時候master崩潰了
  • 3.slave升級成爲master
  • 4.客戶端B在獲取相同資源的鎖時能夠正常獲取(客戶端A和B同時獲取了鎖,違反了安全性)

固然若是你容許兩個客戶端同時持有鎖,那麼這種方案是可行的。不然的話建議使用官方推薦的分佈式鎖方案。dom

單點的正確實現

在嘗試克服上述單實例的限制以前,這裏會用一個簡單的例子來檢查如何完成。 在頻繁的競爭條件的應用中,這也是被接收的方案。由於在單實例中的鎖也是咱們使用分佈式算法的基礎。

爲了得到鎖,能夠這麼作:

SET resource_name my_random_value NX PX 30000
複製代碼

這個命令會在一個值不存在的狀況下設置一個key(NX),這個key會存在30000毫秒(PX)。這個key設置了一個值「myrandomvalue」。這個值針對全部客戶端和鎖必須是惟一的。隨機數是以安全方式分發鎖的的基礎,這經過腳本傳達給redis:若是存在這個key,那麼就移除這個key,而後保存這個value則是我指望的效果。若是我使用Lua腳本實現,將會是:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
複製代碼

爲了不移除其餘客戶端建立的鎖,這是很是必要的。好比一個客戶端得到了鎖,而後在一個相似操做中阻塞了很長時間,而這段時間超過了合法的時間(在這段時間中key已通過期了),隨後移除這個被其餘的客戶得到的鎖。對於一個客戶端來講只是用刪除操做也許是刪除了其餘客戶端所持有的鎖。使用上面的腳本,那麼每一個鎖都是隨機字符串簽名的。因此只有在這個這個客戶端嘗試刪除鎖時,這個鎖纔會被刪除。

和這個隨機的字符串是怎麼產生的呢?我假設這個隨機字符串是從/dev/urandom中取得20個byte,可是你也能用簡易的方法生成惟一的字符串。好比使用/dev/urandom做爲RC4的隨機種子,而後生成一個僞隨機流。一個更簡單的方法是使用一個unix的毫秒數和客戶端id的組合,但這是不安全的,可是能適應大多數環境。

咱們開始設置這個key的存活時間,叫作「鎖的生效時間」。這個時間既是鎖的自動釋放時間,也是其餘客戶端可以再次得到鎖以前已經得到鎖的客戶端可以執行操做的時間。這種狀況沒有在技術上違反互斥保證,只是限制了得到鎖的時間窗口。

因此咱們如今有個不錯的方法來得到和釋放鎖。如今這個非分佈式的單點系統,老是可用的,而且安全的。讓咱們將這種概念擴展到無保護的分佈式系統中。

Redlock算法

在分佈式算法中,咱們假設有N個redis的master。這些node相互獨立,因此我不用複製,也不用任何作任何隱式的協同操做。如今已經在單節點中定義瞭如何得到和釋放鎖。咱們使用算法保證了咱們在單節點中鎖的得到與釋放不會衝突。在咱們的例子中,咱們假設N等於5,這是個合理的值,因此咱們須要在不一樣的機器上運行5臺redis,這也是爲了儘量保證節點相互獨立。

爲了得到鎖,客戶端須要作以下操做:

  • 1.得到當前時間的毫秒數
  • 2.使用相同的key和不一樣的隨機字符串做爲value,嘗試在N個節點中順序得到鎖。在步驟2中,當在一個節點中設置了鎖,那麼客戶端爲了能得到他,將會使用一個總的鎖定時間相比較小的時間做爲超時時間。好比自動釋放的時間是10秒,那麼超時時間爲5-50毫秒。這主要是爲了防止在節點down了後,長時間嘗試得到鎖時的阻塞。若是節點不可用,咱們應該嘗試儘快與下一個節點交互。
  • 3.客戶端經過從當前時間中減去在步驟1中得到的時間戳來計算獲取鎖定所需的時間。當且僅當客戶端可以在大多數實例中獲取鎖定時(至少3個)而且獲取鎖定所通過的總時間小於鎖定有效時間,認爲鎖定被獲取。
  • 4.若是得到了鎖,則其有效時間被認爲是初始有效時間減去通過的時間,如步驟3中計算的。
  • 5.若是客戶端因爲某種緣由(沒法鎖定N / 2 + 1實例或有效時間爲負)沒法獲取鎖定,它將嘗試解鎖全部實例(即便它認爲不是可以鎖定)。

算法是異步的嗎?

該算法依賴於這樣的假設:雖然跨過程沒有同步時鐘,但每一個進程中的本地時間仍然大體以相同的速率流動,其中錯誤與鎖的自動釋放時間相比較小。這個假設很是相似於真實世界的計算機:每臺計算機都有一個本地時鐘,咱們一般能夠依靠不一樣的計算機來得到較小的時鐘漂移。

此時咱們須要更好地指定咱們的互斥規則:只要持有鎖的客戶端將在鎖定有效時間內(在步驟3中得到)終止其工做,減去一些時間(僅幾毫秒),它就獲得保證爲了補償進程之間的時鐘漂移)。 有關須要綁定時鐘漂移的相似系統的更多信息,

這裏有個有趣的參考:Leases: an efficient fault-tolerant mechanism for distributed file cache consistency[dl.acm.org/citation.cf…]

重試失敗

當客戶端沒法獲取鎖定時,它應該在隨機延遲以後再次嘗試,以嘗試同步多個客戶端,同時嘗試獲取同一資源的鎖定(這可能會致使大腦分裂狀況,由於無人可以選取成功)。此外,客戶端嘗試在大多數Redis實例中獲取鎖定的速度越快,分裂大腦條件的窗口越小(而且須要重試),所以理想狀況下客戶端應嘗試將SET命令發送到N個實例同時使用多路複用。

值得強調的是,對於未能得到大多數鎖定的客戶來講,儘快釋放(部分)獲取的鎖定是多麼重要,所以無需等待密鑰到期以便再次獲取鎖定(可是,若是發生網絡分區且客戶端沒法再與Redis實例通訊,則在等待密鑰到期時須要支付可用性懲罰。

釋放鎖

釋放鎖是很簡單的,只需在全部實例中釋放鎖,不管客戶端是否定爲它可以成功鎖定給定實例。

安全論點

算法安全嗎?咱們能夠嘗試瞭解不一樣場景中會發生什麼。 首先讓咱們假設客戶端可以在大多數狀況下獲取鎖。全部實例都將包含一個生存時間相同的密鑰。可是,密鑰設置在不一樣的時間,所以密鑰也將在不一樣的時間到期。可是若是第一個密鑰在時間T1設置爲最差(咱們在聯繫第一個服務器以前採樣的時間),而且最後一個密鑰在時間T2(咱們從最後一個服務器得到回覆的時間)設置爲最差,咱們肯定在集合中到期的第一個密鑰將至少存在MIN_VALIDITY = TTL-(T2-T1)-CLOCK_DRIFT。全部其餘密鑰將在稍後過時,所以咱們確信密鑰將至少同時設置爲此時間。

在那段時間裏,若是設置了大多數密鑰,則另外一個客戶端將沒法獲取鎖定,由於若是已存在N / 2 + 1密鑰,則N / 2 + 1 SET NX操做將沒法成功。 所以,若是得到鎖定,則沒法同時從新獲取鎖定(違反互斥屬性)。

可是,咱們還但願確保同時嘗試獲取鎖的多個客戶端不能同時成功。

若是客戶端使用的時間接近或大於鎖定最大有效時間(咱們基本上用於SET的TTL)鎖定大多數實例,則會認爲鎖定無效並將解鎖實例,所以咱們只須要考慮 客戶端可以在小於有效時間的時間內鎖定大多數實例的狀況。 在這種狀況下,對於上面已經表達的參數,對於MIN_VALIDITY,沒有客戶端應該可以從新獲取鎖。 所以,多個客戶端將可以同時鎖定N / 2 + 1個實例(「時間」爲結束 步驟2)只有當鎖定多數的時間大於TTL時間時,才使鎖定無效。

爭論點

系統活躍度基於三個主要特徵:

  • 1.鎖的自動釋放(由於密鑰到期):最終能夠再次鎖定密鑰。
  • 2.一般狀況下,客戶一般會在未獲取鎖定時或在獲取鎖定而且工做終止時合做移除鎖定,這使得咱們可能沒必要等待密鑰到期以從新獲取鎖。
  • 3.事實上,當客戶端須要重試鎖定時,它等待的時間比獲取大多數鎖定所需的時間要大得多,以便在資源爭用期間機率地分裂大腦條件。

可是,咱們在網絡分區上付出的可用性懲罰等於TTL時間,所以若是有連續分區,咱們能夠無限期地付出此懲罰時間。每次客戶端獲取鎖定並在可以刪除鎖定以前進行分區時,都會發生這種狀況。 基本上若是有無限連續的網絡分區,那麼 系統可能沒法在無限的時間內使用。

性能,崩潰恢復和fsync

使用Redis做爲鎖服務的許多用戶在獲取和釋放鎖的延遲以及每秒可執行的獲取/釋放操做的數量方面須要高性能。爲了知足這一要求,與N個Redis服務器通訊以減小延遲的策略確定是多路複用(或者是妥協的多路複用,即將套接字置於非阻塞模式,發送全部命令,並讀取全部命令稍後,假設客戶端和每一個實例之間的RTT類似)。

可是,若是咱們想要定位崩潰恢復系統模型,還有另外一個考慮持久性的問題。

基本上要看到這裏的問題,讓咱們假設咱們根本沒有持久性配置Redis。客戶端在5個實例中的3箇中獲取鎖定。從新啓動客戶端可以獲取鎖的實例之一,此時還有3個實例能夠鎖定同一資源, 而且另外一個客戶能夠再次鎖定它,違反了鎖定的安全性。

若是咱們啓用AOF持久性,事情會有所改善。 例如,咱們能夠經過發送SHUTDOWN並從新啓動它來升級服務器。 由於Redis過時是在語義上實現的,因此當服務器關閉時,實際上時間仍然流逝了,咱們全部的需求都是實現的很好。 可是一切都很好,只要它是一個簡潔的關閉。 若是停電呢? 若是默認狀況下將Redis配置爲每秒在磁盤上進行fsync,則從新啓動後可能會丟失密鑰。 理論上,若是咱們想要在任何類型的實例重啓時保證鎖定安全性,咱們須要在持久性設置中啓用fsync = always。 這反過來將徹底破壞性能,淪落到傳統上用於以安全方式實現分佈式鎖的CP系統的相同級別。 然而,事情比第一眼看起來更好。 基本上算法是安全的 只要在崩潰後實例從新啓動時,它就會保留,它再也不參與任何當前活動的鎖,所以當實例從新啓動時,當前活動鎖的集合都是經過鎖定除從新加入實例以外的實例得到的系統。

爲了保證這一點,咱們只須要在崩潰後建立一個實例,至少比咱們使用的最大TTL多一點,也就是說,實例崩潰時若是有獲取全部鎖所需的時間,則鎖變爲無效並自動釋放。

使用延遲重啓基本上能夠實現安全性,即便沒有任何可用的Redis持久性,但請注意,這可能轉化爲可用性懲罰。 例如,若是大多數實例崩潰,系統將變爲全局不可用於TTL(這裏全局意味着在此期間根本沒有資源可鎖定)。

使算法更可靠:擴展鎖定

若是客戶端執行的工做由小步驟組成,則默認狀況下可使用較小的鎖定有效時間,並擴展實現鎖定擴展機制的算法。基本上,客戶端若是在鎖定有效性接近低值的狀況下處於計算中間,則能夠經過向全部擴展密鑰的TTL的實例發送Lua腳原本擴展鎖定,若是密鑰存在且其值仍然是獲取鎖定時客戶端分配的隨機值。 若是可以將鎖擴展到大多數實例,而且在有效時間內,基本上使用的算法與獲取鎖時使用的算法很是類似。

客戶端應該只考慮從新獲取的鎖。 可是這在技術上不會更改算法,所以應限制鎖定從新獲取嘗試的最大次數,不然會違反其中一個活動屬性。

相關文章
相關標籤/搜索