在算法的分布式版本中,我們假設(shè)我們有N個Redis主節(jié)點(diǎn)。這些節(jié)點(diǎn)是完全獨(dú)立的足删,因此我們不使用復(fù)制或任何其他隱式協(xié)調(diào)系統(tǒng)。我們已經(jīng)描述了如何在單個實(shí)例中安全地獲取和釋放鎖匣距。我們理所當(dāng)然地認(rèn)為,算法將使用此方法在單個實(shí)例中獲取和釋放鎖哎壳。在我們的示例中毅待,我們設(shè)置了 N=5,這是一個合理的值归榕,因此我們需要在不同的計(jì)算機(jī)或虛擬機(jī)上運(yùn)行 5 個 Redis 主節(jié)點(diǎn)尸红,以確保它們以一種基本獨(dú)立的方式失敗。
為了獲取鎖,客戶端執(zhí)行以下操作:
- 它獲取當(dāng)前時(shí)間(以毫秒為單位)外里。
- 它嘗試按順序獲取所有 N 個實(shí)例中的鎖怎爵,在所有實(shí)例中使用相同的鍵名和隨機(jī)值。在步驟 2 中盅蝗,在每個實(shí)例中設(shè)置鎖定時(shí)鳖链,客戶端使用與總鎖定自動釋放時(shí)間相比較小的超時(shí)來獲取它。例如墩莫,如果自動釋放時(shí)間為 10 秒芙委,則超時(shí)可能在 ~ 5-50 毫秒范圍內(nèi)。這可以防止客戶端長時(shí)間試圖與已關(guān)閉的Redis節(jié)點(diǎn)通信:如果實(shí)例不可用狂秦,我們應(yīng)該盡快嘗試與下一個實(shí)例通信题山。
- 客戶端通過從當(dāng)前時(shí)間中減去步驟 1 中獲得的時(shí)間戳來計(jì)算獲取鎖所經(jīng)過的時(shí)間。當(dāng)且僅當(dāng)客戶端能夠在大多數(shù)實(shí)例(至少 3 個)中獲取鎖故痊,并且獲取鎖所經(jīng)過的總時(shí)間小于鎖的有效時(shí)間,則該鎖被視為已獲取玖姑。
- 如果已獲取鎖愕秫,則其有效性時(shí)間被視為初始有效時(shí)間減去經(jīng)過的時(shí)間,如步驟 3 中計(jì)算的那樣焰络。
- 如果客戶端由于某種原因未能獲取鎖定(它無法鎖定 N/2+1 個實(shí)例或有效期為負(fù)數(shù))戴甩,它將嘗試解鎖所有實(shí)例(甚至是它認(rèn)為無法鎖定的實(shí)例)。
算法是異步的嗎闪彼?
該算法依賴于以下假設(shè):雖然進(jìn)程之間沒有同步時(shí)鐘甜孤,但每個進(jìn)程中的本地時(shí)間以大致相同的速率更新,與鎖的自動釋放時(shí)間相比畏腕,誤差幅度很小缴川。這個假設(shè)與現(xiàn)實(shí)世界的計(jì)算機(jī)非常相似:每臺計(jì)算機(jī)都有一個本地時(shí)鐘,我們通趁柘冢可以依靠不同的計(jì)算機(jī)來具有很小的時(shí)鐘漂移把夸。
在這一點(diǎn)上,我們需要更好地指定我們的互斥規(guī)則:只要持有鎖的客戶端在鎖有效時(shí)間內(nèi)(如步驟3中獲得的那樣)終止其工作铭污,減去一些時(shí)間(只需幾毫秒恋日,以補(bǔ)償進(jìn)程之間的時(shí)鐘漂移),它才能得到保證嘹狞。
本文包含有關(guān)需要綁定時(shí)鐘漂移的類似系統(tǒng)的更多信息:Leases:分布式文件緩存一致性的高效容錯機(jī)制岂膳。
失敗時(shí)重試
當(dāng)客戶端無法獲取鎖時(shí),它應(yīng)該在隨機(jī)延遲后重試磅网,以便嘗試取消同步多個客戶端谈截,嘗試同時(shí)獲取同一資源的鎖(這可能會導(dǎo)致沒有人獲勝的裂腦情況)。此外,客戶端在大多數(shù) Redis 實(shí)例中嘗試獲取鎖的速度越快傻盟,裂腦情況的窗口就越兴偃铩(并且需要重試),因此理想情況下娘赴,客戶端應(yīng)嘗試使用多路復(fù)用同時(shí)將 SET
命令發(fā)送到 N 個實(shí)例规哲。
值得強(qiáng)調(diào)的是,對于未能獲取大多數(shù)鎖的客戶端來說诽表,盡快釋放(部分)獲取的鎖是多么重要唉锌,這樣就不需要等待密鑰到期才能再次獲取鎖(但是,如果發(fā)生網(wǎng)絡(luò)分區(qū)并且客戶端不再能夠與 Redis 實(shí)例通信竿奏, 在等待密鑰過期時(shí)需要支付可用性罰款)袄简。
釋放鎖
釋放鎖很簡單,無論客戶端是否認(rèn)為它能夠成功鎖定給定實(shí)例泛啸,都可以執(zhí)行绿语。
安全論據(jù)
算法安全嗎?讓我們來看看在不同場景中會發(fā)生什么候址。
首先吕粹,我們假設(shè)客戶端能夠在大多數(shù)實(shí)例中獲取鎖。所有實(shí)例都將包含一個具有相同生存時(shí)間的密鑰岗仑。但是匹耕,密鑰是在不同的時(shí)間設(shè)置的,因此密鑰也會在不同的時(shí)間過期荠雕。但是稳其,如果在時(shí)間 T1(我們在聯(lián)系第一臺服務(wù)器之前采樣之前采樣的時(shí)間)將第一個鍵設(shè)置為最差,而在時(shí)間 T2(我們從最后一個服務(wù)器獲得回復(fù)的時(shí)間)將最后一個鍵設(shè)置為最差炸卑,則我們確信集中第一個過期的密鑰將至少存在 既鞠。所有其他密鑰稍后將過期,因此我們確信至少這次將同時(shí)設(shè)置這些密鑰矾兜。MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT
在設(shè)置大多數(shù)密鑰期間损趋,另一個客戶端將無法獲取鎖,因?yàn)槿绻?N/2+1 密鑰已存在椅寺,則 N/2+1 SET NX 操作無法成功浑槽。因此,如果獲得了鎖返帕,則不可能同時(shí)重新獲得它(違反互斥屬性)桐玻。
但是,我們還希望確保嘗試同時(shí)獲取鎖的多個客戶端無法同時(shí)成功荆萤。
如果客戶端鎖定大多數(shù)實(shí)例的時(shí)間接近或大于鎖定最大有效時(shí)間(我們基本上用于 SET 的 TTL)镊靴,它將認(rèn)為鎖定無效并將解鎖實(shí)例铣卡,因此我們只需要考慮客戶端能夠在小于有效時(shí)間的時(shí)間內(nèi)鎖定大多數(shù)實(shí)例的情況。在這種情況下偏竟,對于上面已經(jīng)表達(dá)的參數(shù)煮落,因?yàn)槿魏慰蛻舳硕疾粦?yīng)該能夠重新獲取鎖。因此踊谋,僅當(dāng)鎖定大多數(shù)實(shí)例的時(shí)間大于 TTL 時(shí)間時(shí)蝉仇,多個客戶端才能同時(shí)鎖定 N/2+1 個實(shí)例(“時(shí)間”是步驟 2 的結(jié)束),從而使鎖定無效殖蚕。MIN_VALIDITY
活潑性參數(shù)
系統(tǒng)活動性基于三個主要功能:
- 自動釋放鎖(因?yàn)殍€匙過期):最終鑰匙再次可供鎖定轿衔。
- 事實(shí)上,通常睦疫,當(dāng)未獲取鎖或獲取鎖并終止工作時(shí)害驹,客戶端將合作移除鎖,因此我們可能不必等待密鑰過期即可重新獲取鎖蛤育。
- 事實(shí)上宛官,當(dāng)客戶端需要重試鎖時(shí),它等待的時(shí)間比獲取大多數(shù)鎖所需的時(shí)間要長得多瓦糕,以便從概率上使資源爭用期間的裂腦條件變得不太可能摘刑。
但是,我們支付的可用性損失等于網(wǎng)絡(luò)分區(qū)上的TTL
時(shí)間刻坊,因此,如果有連續(xù)分區(qū)党晋,我們可以無限期地支付此罰款谭胚。每次客戶端獲取鎖并在能夠刪除鎖之前進(jìn)行分區(qū)時(shí),都會發(fā)生這種情況未玻。
基本上灾而,如果存在無限連續(xù)的網(wǎng)絡(luò)分區(qū),則系統(tǒng)可能會在無限長的時(shí)間內(nèi)變得不可用扳剿。
性能旁趟、崩潰恢復(fù)和同步
許多使用 Redis 作為鎖服務(wù)器的用戶在獲取和釋放鎖的延遲以及每秒可以執(zhí)行的獲取/釋放操作數(shù)方面都需要高性能。為了滿足這一要求庇绽,與N Redis服務(wù)器對話以減少延遲的策略肯定是多路復(fù)用(將套接字置于非阻塞模式锡搜,發(fā)送所有命令,稍后讀取所有命令瞧掺,假設(shè)客戶端和每個實(shí)例之間的RTT相似)耕餐。
但是,如果我們想以崩潰恢復(fù)系統(tǒng)模型為目標(biāo)辟狈,則關(guān)于持久性還有另一個考慮因素肠缔。
基本上夏跷,為了看到這里的問題,讓我們假設(shè)我們配置Redis時(shí)根本沒有持久性明未〔刍客戶端在 5 個實(shí)例中的 3 個實(shí)例中獲取鎖√送祝客戶端能夠獲取鎖的其中一個實(shí)例重新啟動猫态,此時(shí)我們可以為同一資源鎖定3個實(shí)例,而另一個客戶端可以再次鎖定它煮纵,這違反了鎖的獨(dú)占性的安全屬性懂鸵。
如果我們啟用AOF持久性,事情將會有所改善行疏。例如匆光,我們可以通過向服務(wù)器發(fā)送 SHUTDOWN
命令并重新啟動它來升級服務(wù)器。由于 Redis 過期在語義上是實(shí)現(xiàn)的酿联,因此當(dāng)服務(wù)器關(guān)閉時(shí)终息,時(shí)間仍然會過去,因此我們所有的要求都很好贞让。但是周崭,只要它是干凈關(guān)閉,一切都很好喳张。停電怎么辦续镇?如果 Redis 配置為(默認(rèn)情況下)每秒在磁盤上同步一次,則重新啟動后销部,我們的密鑰可能會丟失摸航。從理論上講,如果我們想在面對任何類型的實(shí)例重啟時(shí)保證鎖定安全舅桩,我們需要在持久性設(shè)置中啟用酱虎。由于額外的同步開銷,這將影響性能擂涛。fsync=always
然而读串,事情比乍一看要好∪雎瑁基本上恢暖,只要實(shí)例在崩潰后重新啟動,它就不再參與任何當(dāng)前活動的鎖定狰右,算法安全性就會保留胀茵。這意味著實(shí)例重新啟動時(shí)的當(dāng)前活動鎖定集都是通過鎖定重新加入系統(tǒng)的實(shí)例以外的實(shí)例而獲得的。
為了保證這一點(diǎn)挟阻,我們只需要在崩潰后使一個實(shí)例不可用琼娘,至少比我們使用的最大TTL
多一點(diǎn)峭弟。這是實(shí)例崩潰時(shí)存在的有關(guān)鎖的所有密鑰變得無效并自動釋放所需的時(shí)間。
使用延遲重啟脱拼,即使沒有任何可用的Redis持久性瞒瘸,基本上也可以實(shí)現(xiàn)安全,但請注意熄浓,這可能會轉(zhuǎn)化為可用性損失情臭。例如,如果大多數(shù)實(shí)例崩潰赌蔑,系統(tǒng)將全局不可用 TTL
(此處全局意味著在此期間根本沒有資源可鎖定)俯在。
使算法更可靠:擴(kuò)展鎖
如果客戶端執(zhí)行的工作由小步驟組成,則默認(rèn)情況下可以使用較小的鎖定有效期娃惯,并擴(kuò)展實(shí)現(xiàn)鎖定擴(kuò)展機(jī)制的算法跷乐。基本上趾浅,如果客戶端在計(jì)算過程中鎖定有效性接近較低值愕提,則可以通過將Lua腳本發(fā)送到所有實(shí)例來擴(kuò)展鎖定,該實(shí)例擴(kuò)展密鑰的TTL(如果密鑰存在并且其值仍然是客戶端在獲取鎖定時(shí)分配的隨機(jī)值)皿哨。
只有當(dāng)客戶端能夠?qū)㈡i擴(kuò)展到大多數(shù)實(shí)例中浅侨,并且在有效時(shí)間內(nèi)(基本上要使用的算法與獲取鎖時(shí)使用的算法非常相似),才應(yīng)考慮重新獲取鎖证膨。
但是如输,這在技術(shù)上不會更改算法,因此應(yīng)限制鎖定重新獲取嘗試的最大次數(shù)央勒,否則會違反其中一個活動屬性挨决。