一个项目部署多个节点会导致锁失效么_Redis分布式锁
分布式鎖在很多場景中是非常有用的原語, 不同的進程必須以獨占資源的方式實現資源共享就是一個典型的例子。
有很多分布式鎖的庫和描述怎么實現分布式鎖管理器(DLM)的博客,但是每個庫的實現方式都不太一樣,很多庫的實現方式為了簡單降低了可靠性,而有的使用了稍微復雜的設計。
這個頁面試圖提供一個使用Redis實現分布式鎖的規范算法。我們提出一種算法,叫Redlock,我們認為這種實現比普通的單實例實現更安全,我們希望redis社區能幫助分析一下這種實現方法,并給我們提供反饋。
安全和活性失效保障
按照我們的思路和設計方案,算法只需具備3個特性就可以實現一個最低保障的分布式鎖。
為什么基于故障轉移的實現還不夠
為了更好的理解我們想要改進的方面,我們先分析一下當前大多數基于Redis的分布式鎖現狀和實現方法.
實現Redis分布式鎖的最簡單的方法就是在Redis中創建一個key,這個key有一個失效時間(TTL),以保證鎖最終會被自動釋放掉(這個對應特性2)。當客戶端釋放資源(解鎖)的時候,會刪除掉這個key。
從表面上看,似乎效果還不錯,但是這里有一個問題:這個架構中存在一個嚴重的單點失敗問題。如果Redis掛了怎么辦?你可能會說,可以通過增加一個slave節點解決這個問題。但這通常是行不通的。這樣做,我們不能實現資源的獨享,因為Redis的主從同步通常是異步的。
在這種場景(主從結構)中存在明顯的競態:
有時候程序就是這么巧,比如說正好一個節點掛掉的時候,多個客戶端同時取到了鎖。如果你可以接受這種小概率錯誤,那用這個基于復制的方案就完全沒有問題。否則的話,我們建議你實現下面描述的解決方案。
單Redis實例實現分布式鎖的正確方法
在嘗試克服上述單實例設置的限制之前,讓我們先討論一下在這種簡單情況下實現分布式鎖的正確做法,實際上這是一種可行的方案,盡管存在競態,結果仍然是可接受的,另外,這里討論的單實例加鎖方法也是分布式加鎖算法的基礎。
獲取鎖使用命令:
SET resource_name my_random_value NX PX 30000這個命令僅在不存在key的時候才能被執行成功(NX選項),并且這個key有一個30秒的自動失效時間(PX屬性)。這個key的值是“my_random_value”(一個隨機值),這個值在所有的客戶端必須是唯一的,所有同一key的獲取者(競爭者)這個值都不能一樣。
value的值必須是隨機數主要是為了更安全的釋放鎖,釋放鎖的時候使用腳本告訴Redis:只有key存在并且存儲的值和我指定的值一樣才能告訴我刪除成功。可以通過以下Lua腳本實現:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0end使用這種方式釋放鎖可以避免刪除別的客戶端獲取成功的鎖。舉個例子:客戶端A取得資源鎖,但是緊接著被一個其他操作阻塞了,當客戶端A運行完畢其他操作后要釋放鎖時,原來的鎖早已超時并且被Redis自動釋放,并且在這期間資源鎖又被客戶端B再次獲取到。如果僅使用DEL命令將key刪除,那么這種情況就會把客戶端B的鎖給刪除掉。使用Lua腳本就不會存在這種情況,因為腳本僅會刪除value等于客戶端A的value的key(value相當于客戶端的一個簽名)。
這個隨機字符串應該怎么設置?我認為它應該是從/dev/urandom產生的一個20字節隨機數,但是我想你可以找到比這種方法代價更小的方法,只要這個數在你的任務中是唯一的就行。例如一種安全可行的方法是使用/dev/urandom作為RC4的種子和源產生一個偽隨機流;一種更簡單的方法是把以毫秒為單位的unix時間和客戶端ID拼接起來,理論上不是完全安全,但是在多數情況下可以滿足需求.
key的失效時間,被稱作“鎖定有效期”。它不僅是key自動失效時間,而且還是一個客戶端持有鎖多長時間后可以被另外一個客戶端重新獲得。
截至到目前,我們已經有較好的方法獲取鎖和釋放鎖。基于Redis單實例,假設這個單實例總是可用,這種方法已經足夠安全。現在讓我們擴展一下,假設Redis沒有總是可用的保障。
Redlock算法
在Redis的分布式環境中,我們假設有N個Redis master。這些節點完全互相獨立,不存在主從復制或者其他集群協調機制。之前我們已經描述了在Redis單實例下怎么安全地獲取和釋放鎖。我們確保將在每(N)個實例上使用此方法獲取和釋放鎖。在這個樣例中,我們假設有5個Redis master節點,這是一個比較合理的設置,所以我們需要在5臺機器上面或者5臺虛擬機上面運行這些實例,這樣保證他們不會同時都宕掉。
為了取到鎖,客戶端應該執行以下操作:
這個算法是異步的么?
算法基于這樣一個假設:雖然多個進程之間沒有時鐘同步,但每個進程都以相同的時鐘頻率前進,時間差相對于失效時間來說幾乎可以忽略不計。這種假設和我們的真實世界非常接近:每個計算機都有一個本地時鐘,我們可以容忍多個計算機之間有較小的時鐘漂移。
從這點來說,我們必須再次強調我們的互相排斥規則:只有在鎖的有效時間(在步驟3計算的結果)范圍內客戶端能夠做完它的工作,鎖的安全性才能得到保證(鎖的實際有效時間通常要比設置的短,因為計算機之間有時鐘漂移的現象)。.
失敗時重試
當客戶端無法取到鎖時,應該在一個隨機延遲后重試,防止多個客戶端在同時搶奪同一資源的鎖(這樣會導致腦裂,沒有人會取到鎖)。同樣,客戶端取得大部分Redis實例鎖所花費的時間越短,腦裂出現的概率就會越低(必要的重試),所以,理想情況一下,客戶端應該同時(并發地)向所有Redis發送SET命令。
需要強調,當客戶端從大多數Redis實例獲取鎖失敗時,應該盡快地釋放(部分)已經成功取到的鎖,這樣其他的客戶端就不必非得等到鎖過完“有效時間”才能取到(然而,如果已經存在網絡分裂,客戶端已經無法和Redis實例通信,此時就只能等待key的自動釋放了,等于被懲罰了)。
釋放鎖
釋放鎖比較簡單,向所有的Redis實例發送釋放鎖命令即可,不用關心之前有沒有從Redis實例成功獲取到鎖.
安全爭議
這個算法安全么?我們可以從不同的場景討論一下。
讓我們假設客戶端從大多數Redis實例取到了鎖。所有的實例都包含同樣的key,并且key的有效時間也一樣。然而,key肯定是在不同的時間被設置上的,所以key的失效時間也不是精確的相同。我們假設第一個設置的key時間是T1(開始向第一個server發送命令前時間),最后一個設置的key時間是T2(得到最后一臺server的答復后的時間),我們可以確認,第一個server的key至少會存活 MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT。所有其他的key的存活時間,都會比這個key時間晚,所以可以肯定,所有key的失效時間至少是MIN_VALIDITY。
當大部分實例的key被設置后,其他的客戶端將不能再取到鎖,因為至少N/2+1個實例已經存在key。所以,如果一個鎖被(客戶端)獲取后,客戶端自己也不能再次申請到鎖(違反互相排斥屬性)。
然而我們也想確保,當多個客戶端同時搶奪一個鎖時不能兩個都成功。
如果客戶端在獲取到大多數redis實例鎖,使用的時間接近或者已經大于失效時間,客戶端將認為鎖是失效的鎖,并且將釋放掉已經獲取到的鎖,所以我們只需要在有效時間范圍內獲取到大部分鎖這種情況。在上面已經討論過有爭議的地方,在MIN_VALIDITY時間內,將沒有客戶端再次取得鎖。所以只有一種情況,多個客戶端會在相同時間取得N/2+1實例的鎖,那就是取得鎖的時間大于失效時間(TTL time),這樣取到的鎖也是無效的.
如果你能提供關于現有的類似算法的一個正式證明(指出正確性),或者是發現這個算法的bug? 我們將非常感激.
活性爭議
系統的活性安全基于三個主要特性:
然而,當網絡出現問題時系統在失效時間(TTL)內就無法服務,這種情況下我們的程序就會為此付出代價。如果網絡持續的有問題,可能就會出現死循環了。 這種情況發生在當客戶端剛取到一個鎖還沒有來得及釋放鎖就被網絡隔離.
如果網絡一直沒有恢復,這個算法會導致系統不可用.
性能,崩潰恢復和Redis同步
很多用戶把Redis當做分布式鎖服務器,使用獲取鎖和釋放鎖的響應時間,每秒鐘可用執行多少次 acquire / release 操作作為性能指標。為了達到這一要求,增加Redis實例當然可用降低響應延遲(沒有錢買硬件的”窮人”,也可以在網絡方面做優化,使用非阻塞模型,一次發送所有的命令,然后異步的讀取響應結果,假設客戶端和redis服務器之間的RTT都差不多。
然而,如果我們想使用可以從備份中恢復的redis模式,有另外一種持久化情況你需要考慮,.
我們考慮這樣一種場景,假設我們的redis沒用使用備份。一個客戶端獲取到了3個實例的鎖。此時,其中一個已經被客戶端取到鎖的redis實例被重啟,在這個時間點,就可能出現3個節點沒有設置鎖,此時如果有另外一個客戶端來設置鎖,鎖就可能被再次獲取到,這樣鎖的互相排斥的特性就被破壞掉了。
如果我們啟用了AOF持久化,情況會好很多。我們可用使用SHUTDOWN命令關閉然后再次重啟。因為Redis到期是語義上實現的,所以當服務器關閉時,實際上還是經過了時間,所有(保持鎖)需要的條件都沒有受到影響. 沒有受到影響的前提是redis優雅的關閉。停電了怎么辦?如果redis是每秒執行一次fsync,那么很有可能在redis重啟之后,key已經丟棄。理論上,如果我們想在Redis重啟地任何情況下都保證鎖的安全,我們必須開啟fsync=always的配置。這反過來將完全破壞與傳統上用于以安全的方式實現分布式鎖的同一級別的CP系統的性能.
然而情況總比一開始想象的好一些。當一個redis節點重啟后,只要它不參與到任意當前活動的鎖,沒有被當做“當前存活”節點被客戶端重新獲取到,算法的安全性仍然是有保障的。
為了達到這種效果,我們只需要將新的redis實例,在一個TTL時間內,對客戶端不可用即可,在這個時間內,所有客戶端鎖將被失效或者自動釋放.
使用延遲重啟可以在不采用持久化策略的情況下達到同樣的安全,然而這樣做有時會讓系統轉化為徹底不可用。比如大部分的redis實例都崩潰了,系統在TTL時間內任何鎖都將無法加鎖成功。
使算法更加可靠:鎖的擴展
如果你的工作可以拆分為許多小步驟,可以將有效時間設置的小一些,使用鎖的一些擴展機制。在工作進行的過程中,當發現鎖剩下的有效時間很短時,可以再次向redis的所有實例發送一個Lua腳本,讓key的有效時間延長一點(前提還是key存在并且value是之前設置的value)。
客戶端擴展TTL時必須像首次取得鎖一樣在大多數實例上擴展成功才算再次取到鎖,并且是在有效時間內再次取到鎖(算法和獲取鎖是非常相似的)。
這樣做從技術上將并不會改變算法的正確性,所以擴展鎖的過程中仍然需要達到獲取到N/2+1個實例這個要求,否則活性特性之一就會失效。
總結
以上是生活随笔為你收集整理的一个项目部署多个节点会导致锁失效么_Redis分布式锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python-louvain
- 下一篇: mysql数据库持续_MySql数据库-