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