前言
在之前的《基于redis的分布式鎖設(shè)計實(shí)現(xiàn)》文章中,介紹并實(shí)現(xiàn)了兩種常見的redis分布式鎖。但這種方式僅能保證在一個單節(jié)點(diǎn)的、保證永不宕機(jī)的環(huán)境下沒有任何問題。在redis集群中,若遇到極端特殊場景會出現(xiàn)一些問題。
為什么說之前的分布式鎖設(shè)計有問題?
用Redis來實(shí)現(xiàn)分布式鎖最簡單的方式就是在實(shí)例里創(chuàng)建一個鍵值,創(chuàng)建出來的鍵值一般都是有一個超時時間的,所以每個鎖最終都會釋放。而當(dāng)一個客戶端想要釋放鎖時,它只需要刪除這個鍵值即可。
表面來看,這個方法似乎很管用,但是這里存在一個問題:在真實(shí)業(yè)務(wù)場景,為了保證緩存系統(tǒng)的高可用,redis往往并非是單點(diǎn)的,而是集群部署的。由于redis主從節(jié)點(diǎn)的數(shù)據(jù)同步是異步的,如果Redis的master節(jié)點(diǎn)在鎖未同步到Slave節(jié)點(diǎn)的時候宕機(jī)了怎么辦?舉例來說:
- 客戶端A在master節(jié)點(diǎn)獲得了鎖。
- 在鎖同步到slave之前,master宕機(jī),還未來得及將鎖同步到slave
- slave變成了master節(jié)點(diǎn)
- 客戶端B也得到了和A持有的相同的鎖
在這種情況下,如果你可以容忍在宕機(jī)期間,多個客戶端允許同時都持有鎖,那用這個基于復(fù)制的方案就完全沒有問題,否則上一篇文章實(shí)現(xiàn)的分布式鎖明顯是不可行的,因為這種方案無法保證分布式鎖的安全和可靠性保證的第1個安全互斥屬性
分布式鎖的安全和可靠性保證需要滿足以下三個屬性:
- 一致性:互斥,不管任何時候,只有一個客戶端能持有同一個鎖。
- 分區(qū)可容忍性:不會死鎖,最終一定會得到鎖,就算一個持有鎖的客戶端宕掉或者發(fā)生網(wǎng)絡(luò)分區(qū)。
- 可用性:只要大多數(shù)Redis節(jié)點(diǎn)正常工作,客戶端應(yīng)該都能獲取和釋放鎖。
此外,之前的設(shè)計的另一個問題是,當(dāng)占有鎖的線程執(zhí)行時間大于過期時間,此時另一個線程也獲取了鎖,導(dǎo)致兩個線程可同時訪問共享資源。
RedLock 算法介紹
假設(shè)我們有N個Redis master節(jié)點(diǎn),這些節(jié)點(diǎn)完全獨(dú)立,不使用任何復(fù)制或者其他隱含的分布式協(xié)調(diào)算法。因此我們用之前在單節(jié)點(diǎn)環(huán)境下安全地獲取和釋放鎖的方法在每個單節(jié)點(diǎn)里來獲取和釋放鎖。
注意!?。。?/strong>redLock會直接連接多個redis節(jié)點(diǎn),不是通過集群機(jī)制連接的,RedLock的寫與主從集群無關(guān),直接操作的是所有主節(jié)點(diǎn),所以才能避開主從故障切換時鎖丟失的問題。
我們把N假設(shè)成5,這個數(shù)字是一個相對比較合理的數(shù)值,因此我們需要在不同的計算機(jī)或者虛擬機(jī)上運(yùn)行5個master節(jié)點(diǎn)來保證他們大多數(shù)情況下都不會同時宕機(jī)。一個客戶端需要做如下操作來獲取鎖:
- 獲取當(dāng)前時間(單位是毫秒)
- 按順序用相同的key和隨機(jī)值依次向N個Redis請求鎖。在這一步里,客戶端在每個master上請求鎖時,有一個遠(yuǎn)小于鎖釋放時間的超時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節(jié)點(diǎn)鎖請求的超時時間可能是5-50毫秒的范圍,防止一個客戶端在某個宕掉的master節(jié)點(diǎn)上阻塞過長時間,如果一個master節(jié)點(diǎn)不可用了,則應(yīng)該盡快嘗試下一個master節(jié)點(diǎn)。
- 客戶端計算獲取鎖總共花了多少時間,只有當(dāng)客戶端在大多數(shù)master節(jié)點(diǎn)上成功獲取了鎖(在這里是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認(rèn)為是獲取成功了。
- 如果鎖獲取成功了,鎖的有效時間就是最初的鎖釋放時間減去獲取鎖所消耗的時間。
- 如果鎖獲取失敗了,不管是因為獲取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,客戶端都會到每個master節(jié)點(diǎn)上釋放鎖,即便是那些他認(rèn)為沒有獲取成功的鎖。
失敗的重試
當(dāng)一個客戶端獲取鎖失敗時,這個客戶端應(yīng)該在一個隨機(jī)延時后進(jìn)行重試。采用隨機(jī)延時是為了避免不同客戶端同時重試導(dǎo)致誰都無法拿到鎖的情況出現(xiàn),同樣的道理客戶端越快嘗試在大多數(shù)Redis節(jié)點(diǎn)獲取鎖,出現(xiàn)多個客戶端同時競爭鎖和重試的時間窗口越小,可能性就越低,所以最完美的情況下,客戶端應(yīng)該用多路傳輸?shù)姆绞酵瑫r向所有Redis節(jié)點(diǎn)發(fā)送SET命令。 這里非常有必要強(qiáng)調(diào)一下客戶端如果沒有在多數(shù)節(jié)點(diǎn)獲取到鎖,一定要盡快在獲取鎖成功的節(jié)點(diǎn)上釋放鎖,這樣就沒必要等到key超時后才能重新獲取這個鎖(但是如果網(wǎng)絡(luò)分區(qū)的情況發(fā)生而且客戶端無法連接到Redis節(jié)點(diǎn)時,會損失等待key超時這段時間的系統(tǒng)可用性)
釋放鎖
釋放鎖比較簡單,因為只需要在所有節(jié)點(diǎn)都釋放鎖就行,不管之前有沒有在該節(jié)點(diǎn)獲取鎖成功。
RedLock算法缺點(diǎn)
Redlock算法對時鐘依賴性太強(qiáng),若N個節(jié)點(diǎn)中的某個節(jié)點(diǎn)發(fā)生時間跳躍,也可能會引此而引發(fā)鎖安全性問題。
可參閱文章怎樣做可靠的分布式鎖,Redlock 真的可行么?,Redis RedLock 完美的分布式鎖么?
文章里例舉了個因為時間問題,Redlock 不可靠的例子。
- client1 從 ABC 三個節(jié)點(diǎn)處申請到鎖,DE由于網(wǎng)絡(luò)原因請求沒有到達(dá)
- C節(jié)點(diǎn)的時鐘往前推了,導(dǎo)致 lock 過期
- client2 在CDE處獲得了鎖,AB由于網(wǎng)絡(luò)原因請求未到達(dá)
- 此時 client1 和 client2 都獲得了鎖
在 Redlock 官方文檔中也提到了這個情況,不過是C崩潰的時候,Redlock 官方本身也是知道 Redlock 算法不是完全可靠的,官方為了解決這種問題建議使用延時啟動,相關(guān)內(nèi)容可以看之前的這篇文章。但是 Martin 這里分析得更加全面,指出延時啟動不也是依賴于時鐘的正確性的么?
僅有在你假設(shè)了一個同步性系統(tǒng)模型的基礎(chǔ)上,Redlock 才能正常工作,也就是系統(tǒng)能滿足以下屬性:
- 網(wǎng)絡(luò)延時邊界,即假設(shè)數(shù)據(jù)包一定能在某個最大延時之內(nèi)到達(dá)
- 進(jìn)程停頓邊界,即進(jìn)程停頓一定在某個最大時間之內(nèi)
- 時鐘錯誤邊界,即不會從一個壞的 NTP 服務(wù)器處取得時間
Martin 認(rèn)為 Redlock 實(shí)在不是一個好的選擇,對于需求性能的分布式鎖應(yīng)用它太重了且成本高;對于需求正確性的應(yīng)用來說它不夠安全。因為它對高危的時鐘或者說其他上述列舉的情況進(jìn)行了不可靠的假設(shè),如果你的應(yīng)用只需要高性能的分布式鎖不要求多高的正確性,那么單節(jié)點(diǎn) Redis 夠了;如果你的應(yīng)用想要保住正確性,那么不建議 Redlock,建議使用一個合適的一致性協(xié)調(diào)系統(tǒng),例如 Zookeeper,且保證存在 fencing token。
不過筆者認(rèn)為,應(yīng)用場景也不是那么絕對,對于性能與正確性,有時候只需要一個折中的方案,保證較高的正確性的同時保證較高的性能,所以是否使用RedLock還取決于適不適合應(yīng)用場景