Redis分佈式鎖

Redis分佈式鎖

分佈式鎖是許多環境中很是有用的原語,其中不一樣的進程必須以相互排斥的方式與共享資源一塊兒運行。php

有許多圖書館和博客文章描述瞭如何使用Redis實現DLM(分佈式鎖管理器),可是每一個庫都使用不一樣的方法,而且許多使用一種簡單的方法,與稍微更復雜的方法相比能夠實現更低的保證設計。node

這個頁面試圖提供一個更規範的算法來實現與Redis的分佈式鎖。咱們提出一種稱爲Redlock的算法,它實現了一種DLM,咱們認爲它比香草單實例方法更安全。咱們但願社區可以對其進行分析,提供反饋,並將其用做實施或更復雜或替代設計的起點。git

實施

在描述算法以前,這裏有一些可用於參考的可用實現的連接。github

安全生活保障

咱們將使用三個屬性來建模咱們的設計,從咱們的角度來看,是以有效的方式使用分佈式鎖所需的最低保證。面試

  1. 安全屬性:相互排斥。在任何給定的時刻,只有一個客戶端能夠持有鎖。
  2. 活力財產A:無死鎖 最終,即便鎖定資源的客戶端崩潰或得到分區,始終能夠獲取鎖定。
  3. 活力屬性B:容錯。只要大部分Redis節點都啓動,客戶端就能夠獲取和釋放鎖。

爲何基於故障轉移的實現是不夠的

要了解咱們想要改進的內容,咱們來分析大多數基於Redis的分佈式鎖庫的當前狀態。redis

使用Redis鎖定資源的最簡單方法是在實例中建立一個密鑰。關鍵是一般在有限的時間內使用Redis到期功能建立,以便最終將其釋放(咱們列表中的屬性2)。當客戶端須要釋放資源時,會刪除該密鑰。算法

表面上這樣作很好,可是有一個問題:這是咱們的架構中的一個失敗點。若是Redis主人失敗會怎麼樣?好吧,讓咱們添加一個奴隸!若是主機不可用,請使用它。這不幸是不可行的。經過這樣作,咱們不能實現咱們的互斥的安全屬性,由於Redis複製是異步的。緩存

這種模式有明顯的競爭條件:安全

  1. 客戶端A獲取主機中的鎖。
  2. 主機在寫入密鑰以前發生故障,並將其傳送到從站。
  3. 奴隸被提拔爲主人。
  4. 客戶端B獲取對同一資源的鎖定A已經擁有鎖。安全隱患!

有時徹底能夠在特殊狀況下,如失敗時,多個客戶端能夠同時鎖住鎖。若是是這種狀況,您可使用基於複製的解決方案。不然咱們建議實施本文檔中描述的解決方案。服務器

使用單個實例正確實現

在嘗試克服上述單一實例設置的限制以前,讓咱們在這種簡單的狀況下檢查如何正確執行,由於這其實是一個可行的解決方案,在應用程序中,不時有競爭條件是能夠接受的,而且由於鎖定單個實例是咱們將用於此處描述的分佈式算法的基礎。

要得到鎖,路要走以下:

SET resource_name my_random_value NX PX 30000

該命令將僅在不存在(NX選項)的狀況下設置密鑰,而且超時時間爲30000毫秒(PX選項)。鍵被設置爲值「個人隨機值」。該值在全部客戶端和全部鎖定請求中必須是惟一的。

基本上使用隨機值以便以安全的方式釋放鎖,而且使用一個腳原本告訴Redis:只有當它存在而且存儲在密鑰上的值正好是我指望的值時才刪除該鍵。這是由如下Lua腳本完成的:

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

這是爲了不刪除由另外一個客戶端建立的鎖重要。例如,客戶端能夠獲取鎖定,在某些操做中被阻塞的時間長於鎖定有效時間(密鑰到期的時間),而後刪除已被其餘客戶端獲取的鎖定。使用DEL是不安全的,由於客戶端可能會刪除另外一個客戶端的鎖定。使用上述腳本,而不是每一個鎖都使用隨機字符串「簽名」,所以只有當客戶端嘗試將其刪除時,該鎖纔會被刪除。

這個隨機字符串應該是什麼?我假設它是/ dev / urandom的20個字節,可是你能夠找到更便宜的方法來使其獨特,足以知足你的任務。例如,安全的選擇是使用/ dev / urandom種子RC4,並從中生成僞隨機流。一個更簡單的解決方案是使用unix時間與微秒分辨率的組合,將其與客戶端ID鏈接起來,它不是那麼安全,但可能在大多數環境中的任務。

咱們用做關鍵時間的時間被稱爲「鎖定有效期」。不管是自動發佈時間仍是客戶端爲了執行所需的操做,在另外一個客戶端能夠再次獲取鎖定以前,客戶端都有時間,而不會在技術上違反互斥保證,這僅限於給定的窗口從得到鎖定的時刻開始。

因此如今咱們有一個很好的方式來獲取和釋放鎖。系統,關於由單一的,始終可用的實例組成的非分佈式系統的推理是安全的。咱們將這個概念擴展到一個咱們沒有這種保證的分佈式系統。

Redlock算法

在分佈式版本的算法中,咱們假設咱們有N Redis主控。那些節點是徹底獨立的,因此咱們不使用複製或任何其餘隱式協調系統。咱們已經描述瞭如何在一個實例中安全地獲取和釋放鎖。咱們認爲算法將使用此方法在單個實例中獲取和釋放鎖。在咱們的例子中,咱們設置N = 5,這是一個合理的值,因此咱們須要在不一樣的計算機或虛擬機上運行5個Redis主人,以確保它們以最獨立的方式失敗。

爲了獲取鎖定,客戶端執行如下操做:

  1. 它以毫秒爲單位獲取當前時間。
  2. 它嘗試在全部N個實例中順序獲取鎖,在全部實例中使用相同的密鑰名稱和隨機值。在步驟2中,當在每一個實例中設置鎖定時,客戶端使用與總鎖定自動釋放時間相比較小的超時,以獲取它。例如,若是自動釋放時間爲10秒,超時可能在〜5-50毫秒範圍內。這樣能夠防止客戶端長時間阻止與Redis節點進行通話:若是一個實例不可用,咱們應該嘗試儘快與下一個實例通話。
  3. 客戶端計算經過從當前時間中減去步驟1中得到的時間戳來獲取鎖定所需的時間。當且僅當客戶端可以在大多數實例中獲取鎖定(至少3)時, ,而且獲取鎖定的總時間小於鎖定有效時間,則認爲該鎖被獲取。
  4. 若是鎖被獲取,則其有效時間被認爲是初始有效時間減去通過的時間,如步驟3中所計算的。
  5. 若是客戶端因爲某種緣由(沒法鎖定N / 2 + 1個實例或有效時間爲負)而沒法獲取鎖定,則會嘗試解鎖全部實例(即便是相信它不是可以鎖定)。

算法是否異步?

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

在這一點上,咱們須要更好地指定咱們的互斥規則:只要持有鎖的客戶端將在鎖定有效時間內終止其工做(如步驟3中得到的),減去一段時間(僅幾毫秒)以補償進程之間的時鐘漂移)。

有關須要綁定時鐘漂移的相似系統的更多信息,本文是一個有趣的參考:租賃:分佈式文件緩存一致性的高效容錯機制

重試失敗

當客戶端沒法獲取鎖定時,應該在隨機延遲以後再次嘗試從新嘗試從新同步多個客戶端嘗試同時獲取同一資源的鎖定(這可能會致使大腦失敗的狀況,勝利)。客戶端嘗試獲取大多數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沒有客戶端應該可以從新得到鎖。所以,只有當鎖定多數的時間大於TTL時間時,多個客戶端才能同時鎖定N / 2 + 1個實例(「時間」是步驟2的結束),使鎖定無效。

您可否提供正式的安全證實,指出現有的相似的算法,仍是發現錯誤?這將很是感激。

生活論證

系統活動基於三個主要特色:

  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腳原本擴展鎖定,若是該鍵存在而且其值仍然是獲取鎖時客戶端分配的隨機值。

若是客戶端可以將鎖擴展到大多數實例,而且在有效時間內(基本上使用的算法與獲取鎖定時使用的算法基本相同),客戶端應該僅考慮從新獲取的鎖。

然而,這並不技術地改變算法,因此鎖定從新獲取嘗試的最大數目應受到限制,不然其中一個活動屬性被違反。

相關文章
相關標籤/搜索