網(wǎng)上有關(guān)Redis分布式鎖的文章可謂多如牛毛了,不信的話你可以拿關(guān)鍵詞“Redis 分布式鎖”隨便到哪個(gè)搜索引擎上去搜索一下就知道了涕烧。這些文章的思路大體相近,給出的實(shí)現(xiàn)算法也看似合乎邏輯汗洒,但當(dāng)我們著手去實(shí)現(xiàn)它們的時(shí)候议纯,卻發(fā)現(xiàn)如果你越是仔細(xì)推敲,疑慮也就越來越多仲翎。
實(shí)際上痹扇,大概在一年以前,關(guān)于Redis分布式鎖的安全性問題溯香,在分布式系統(tǒng)專家Martin Kleppmann和Redis的作者antirez之間就發(fā)生過一場爭論。由于對這個(gè)問題一直以來比較關(guān)注浓恶,所以我前些日子仔細(xì)閱讀了與這場爭論相關(guān)的資料玫坛。這場爭論的大概過程是這樣的:為了規(guī)范各家對基于Redis的分布式鎖的實(shí)現(xiàn),Redis的作者提出了一個(gè)更安全的實(shí)現(xiàn)包晰,叫做Redlock湿镀。有一天,Martin Kleppmann寫了一篇blog伐憾,分析了Redlock在安全性上存在的一些問題勉痴。然后Redis的作者立即寫了一篇blog來反駁Martin的分析。但Martin表示仍然堅(jiān)持原來的觀點(diǎn)树肃。隨后蒸矛,這個(gè)問題在Twitter和Hacker News上引發(fā)了激烈的討論,很多分布式系統(tǒng)的專家都參與其中胸嘴。
對于那些對分布式系統(tǒng)感興趣的人來說雏掠,這個(gè)事件非常值得關(guān)注。不管你是剛接觸分布式系統(tǒng)的新手劣像,還是有著多年分布式開發(fā)經(jīng)驗(yàn)的老手乡话,讀完這些分析和評論之后,大概都會有所收獲耳奕。要知道绑青,親手實(shí)現(xiàn)過Redis Cluster這樣一個(gè)復(fù)雜系統(tǒng)的antirez诬像,足以算得上分布式領(lǐng)域的一名專家了。但對于由分布式鎖引發(fā)的一系列問題的分析中闸婴,不同的專家卻能得出迥異的結(jié)論坏挠,從中我們可以窺見分布式系統(tǒng)相關(guān)的問題具有何等的復(fù)雜性。實(shí)際上掠拳,在分布式系統(tǒng)的設(shè)計(jì)中經(jīng)常發(fā)生的事情是:許多想法初看起來毫無破綻癞揉,而一旦詳加考量,卻發(fā)現(xiàn)不是那么天衣無縫溺欧。
下面喊熟,我們就從頭至尾把這場爭論過程中各方的觀點(diǎn)進(jìn)行一下回顧和分析。在這個(gè)過程中姐刁,我們把影響分布式鎖的安全性的那些技術(shù)細(xì)節(jié)展開進(jìn)行討論芥牌,這將是一件很有意思的事情。這也是一個(gè)比較長的故事聂使。當(dāng)然壁拉,其中也免不了包含一些小“八卦”。
Redlock算法
就像本文開頭所講的柏靶,借助Redis來實(shí)現(xiàn)一個(gè)分布式鎖(Distributed Lock)的做法弃理,已經(jīng)有很多人嘗試過。人們構(gòu)建這樣的分布式鎖的目的屎蜓,是為了對一些共享資源進(jìn)行互斥訪問痘昌。
但是,這些實(shí)現(xiàn)雖然思路大體相近炬转,但實(shí)現(xiàn)細(xì)節(jié)上各不相同辆苔,它們能提供的安全性和可用性也不盡相同。所以扼劈,Redis的作者antirez給出了一個(gè)更好的實(shí)現(xiàn)驻啤,稱為Redlock,算是Redis官方對于實(shí)現(xiàn)分布式鎖的指導(dǎo)規(guī)范荐吵。Redlock的算法描述就放在Redis的官網(wǎng)上:https://redis.io/topics/distlock
在Redlock之前骑冗,很多人對于分布式鎖的實(shí)現(xiàn)都是基于單個(gè)Redis節(jié)點(diǎn)的。而Redlock是基于多個(gè)Redis節(jié)點(diǎn)(都是Master)的一種實(shí)現(xiàn)捍靠。為了能理解Redlock沐旨,我們首先需要把簡單的基于單Redis節(jié)點(diǎn)的算法描述清楚,因?yàn)樗荝edlock的基礎(chǔ)榨婆。
基于單Redis節(jié)點(diǎn)的分布式鎖
首先磁携,Redis客戶端為了獲取鎖,向Redis節(jié)點(diǎn)發(fā)送如下命令:
SET resource_name my_random_value NX PX 30000
上面的命令如果執(zhí)行成功良风,則客戶端成功獲取到了鎖谊迄,接下來就可以訪問共享資源了闷供;而如果上面的命令執(zhí)行失敗,則說明獲取鎖失敗统诺。
注意歪脏,在上面的SET命令中:
- my_random_value是由客戶端生成的一個(gè)隨機(jī)字符串,它要保證在足夠長的一段時(shí)間內(nèi)在所有客戶端的所有獲取鎖的請求中都是唯一的粮呢。
- NX表示只有當(dāng)resource_name對應(yīng)的key值不存在的時(shí)候才能SET成功婿失。這保證了只有第一個(gè)請求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖啄寡。
- PX 30000表示這個(gè)鎖有一個(gè)30秒的自動過期時(shí)間豪硅。當(dāng)然,這里30秒只是一個(gè)例子挺物,客戶端可以選擇合適的過期時(shí)間懒浮。
最后,當(dāng)客戶端完成了對共享資源的操作之后识藤,執(zhí)行下面的Redis Lua腳本來釋放鎖:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
這段Lua腳本在執(zhí)行的時(shí)候要把前面的my_random_value作為ARGV[1]的值傳進(jìn)去砚著,把resource_name作為KEYS[1]的值傳進(jìn)去。
至此痴昧,基于單Redis節(jié)點(diǎn)的分布式鎖的算法就描述完了稽穆。這里面有好幾個(gè)問題需要重點(diǎn)分析一下。
首先第一個(gè)問題赶撰,這個(gè)鎖必須要設(shè)置一個(gè)過期時(shí)間秧骑。否則的話,當(dāng)一個(gè)客戶端獲取鎖成功之后扣囊,假如它崩潰了,或者由于發(fā)生了網(wǎng)絡(luò)分割(network partition)導(dǎo)致它再也無法和Redis節(jié)點(diǎn)通信了绒疗,那么它就會一直持有這個(gè)鎖侵歇,而其它客戶端永遠(yuǎn)無法獲得鎖了。antirez在后面的分析中也特別強(qiáng)調(diào)了這一點(diǎn)吓蘑,而且把這個(gè)過期時(shí)間稱為鎖的有效時(shí)間(lock validity time)惕虑。獲得鎖的客戶端必須在這個(gè)時(shí)間之內(nèi)完成對共享資源的訪問。
第二個(gè)問題磨镶,第一步獲取鎖的操作溃蔫,網(wǎng)上不少文章把它實(shí)現(xiàn)成了兩個(gè)Redis命令:
SETNX resource_name my_random_value
EXPIRE resource_name 30
雖然這兩個(gè)命令和前面算法描述中的一個(gè)SET命令執(zhí)行效果相同,但卻不是原子的琳猫。如果客戶端在執(zhí)行完SETNX后崩潰了伟叛,那么就沒有機(jī)會執(zhí)行EXPIRE了,導(dǎo)致它一直持有這個(gè)鎖脐嫂。
第三個(gè)問題统刮,也是antirez指出的紊遵,設(shè)置一個(gè)隨機(jī)字符串my_random_value是很有必要的,它保證了一個(gè)客戶端釋放的鎖必須是自己持有的那個(gè)鎖侥蒙。假如獲取鎖時(shí)SET的不是一個(gè)隨機(jī)字符串暗膜,而是一個(gè)固定值,那么可能會發(fā)生下面的執(zhí)行序列:
- 客戶端1獲取鎖成功鞭衩。
- 客戶端1在某個(gè)操作上阻塞了很長時(shí)間学搜。
- 過期時(shí)間到了,鎖自動釋放了论衍。
- 客戶端2獲取到了對應(yīng)同一個(gè)資源的鎖瑞佩。
- 客戶端1從阻塞中恢復(fù)過來,釋放掉了客戶端2持有的鎖饲齐。
之后钉凌,客戶端2在訪問共享資源的時(shí)候,就沒有鎖為它提供保護(hù)了捂人。
第四個(gè)問題御雕,釋放鎖的操作必須使用Lua腳本來實(shí)現(xiàn)。釋放鎖其實(shí)包含三步操作:’GET’滥搭、判斷和’DEL’酸纲,用Lua腳本來實(shí)現(xiàn)能保證這三步的原子性。否則瑟匆,如果把這三步操作放到客戶端邏輯中去執(zhí)行的話闽坡,就有可能發(fā)生與前面第三個(gè)問題類似的執(zhí)行序列:
- 客戶端1獲取鎖成功。
- 客戶端1訪問共享資源愁溜。
- 客戶端1為了釋放鎖疾嗅,先執(zhí)行’GET’操作獲取隨機(jī)字符串的值。
- 客戶端1判斷隨機(jī)字符串的值冕象,與預(yù)期的值相等代承。
- 客戶端1由于某個(gè)原因阻塞住了很長時(shí)間。
- 過期時(shí)間到了渐扮,鎖自動釋放了论悴。
- 客戶端2獲取到了對應(yīng)同一個(gè)資源的鎖。
- 客戶端1從阻塞中恢復(fù)過來墓律,執(zhí)行DEL操縱膀估,釋放掉了客戶端2持有的鎖。
實(shí)際上耻讽,在上述第三個(gè)問題和第四個(gè)問題的分析中察纯,如果不是客戶端阻塞住了,而是出現(xiàn)了大的網(wǎng)絡(luò)延遲,也有可能導(dǎo)致類似的執(zhí)行序列發(fā)生捐寥。
前面的四個(gè)問題笤昨,只要實(shí)現(xiàn)分布式鎖的時(shí)候加以注意,就都能夠被正確處理握恳。但除此之外瞒窒,antirez還指出了一個(gè)問題,是由failover引起的乡洼,卻是基于單Redis節(jié)點(diǎn)的分布式鎖無法解決的崇裁。正是這個(gè)問題催生了Redlock的出現(xiàn)。
這個(gè)問題是這樣的束昵。假如Redis節(jié)點(diǎn)宕機(jī)了拔稳,那么所有客戶端就都無法獲得鎖了,服務(wù)變得不可用锹雏。為了提高可用性巴比,我們可以給這個(gè)Redis節(jié)點(diǎn)掛一個(gè)Slave,當(dāng)Master節(jié)點(diǎn)不可用的時(shí)候礁遵,系統(tǒng)自動切到Slave上(failover)轻绞。但由于Redis的主從復(fù)制(replication)是異步的,這可能導(dǎo)致在failover過程中喪失鎖的安全性佣耐≌考慮下面的執(zhí)行序列:
- 客戶端1從Master獲取了鎖。
- Master宕機(jī)了兼砖,存儲鎖的key還沒有來得及同步到Slave上奸远。
- Slave升級為Master。
- 客戶端2從新的Master獲取到了對應(yīng)同一個(gè)資源的鎖讽挟。
于是懒叛,客戶端1和客戶端2同時(shí)持有了同一個(gè)資源的鎖。鎖的安全性被打破耽梅。針對這個(gè)問題芍瑞,antirez設(shè)計(jì)了Redlock算法,我們接下來會討論褐墅。
【其它疑問】
前面這個(gè)算法中出現(xiàn)的鎖的有效時(shí)間(lock validity time),設(shè)置成多少合適呢洪己?如果設(shè)置太短的話妥凳,鎖就有可能在客戶端完成對于共享資源的訪問之前過期,從而失去保護(hù)答捕;如果設(shè)置太長的話逝钥,一旦某個(gè)持有鎖的客戶端釋放鎖失敗,那么就會導(dǎo)致所有其它客戶端都無法獲取鎖,從而長時(shí)間內(nèi)無法正常工作艘款〕旨剩看來真是個(gè)兩難的問題。
而且哗咆,在前面對于隨機(jī)字符串my_random_value的分析中蜘欲,antirez也在文章中承認(rèn)的確應(yīng)該考慮客戶端長期阻塞導(dǎo)致鎖過期的情況。如果真的發(fā)生了這種情況晌柬,那么共享資源是不是已經(jīng)失去了保護(hù)呢姥份?antirez重新設(shè)計(jì)的Redlock是否能解決這些問題呢?
分布式鎖Redlock
由于前面介紹的基于單Redis節(jié)點(diǎn)的分布式鎖在failover的時(shí)候會產(chǎn)生解決不了的安全性問題年碘,因此antirez提出了新的分布式鎖的算法Redlock澈歉,它基于N個(gè)完全獨(dú)立的Redis節(jié)點(diǎn)(通常情況下N可以設(shè)置成5)。
運(yùn)行Redlock算法的客戶端依次執(zhí)行下面各個(gè)步驟屿衅,來完成獲取鎖的操作:
- 獲取當(dāng)前時(shí)間(毫秒數(shù))埃难。
- 按順序依次向N個(gè)Redis節(jié)點(diǎn)執(zhí)行獲取鎖的操作。這個(gè)獲取操作跟前面基于單Redis節(jié)點(diǎn)的獲取鎖的過程相同涤久,包含隨機(jī)字符串my_random_value涡尘,也包含過期時(shí)間(比如PX 30000,即鎖的有效時(shí)間)拴竹。為了保證在某個(gè)Redis節(jié)點(diǎn)不可用的時(shí)候算法能夠繼續(xù)運(yùn)行悟衩,這個(gè)獲取鎖的操作還有一個(gè)超時(shí)時(shí)間(time out),它要遠(yuǎn)小于鎖的有效時(shí)間(幾十毫秒量級)栓拜∽荆客戶端在向某個(gè)Redis節(jié)點(diǎn)獲取鎖失敗以后,應(yīng)該立即嘗試下一個(gè)Redis節(jié)點(diǎn)幕与。這里的失敗挑势,應(yīng)該包含任何類型的失敗,比如該Redis節(jié)點(diǎn)不可用啦鸣,或者該Redis節(jié)點(diǎn)上的鎖已經(jīng)被其它客戶端持有(注:Redlock原文中這里只提到了Redis節(jié)點(diǎn)不可用的情況潮饱,但也應(yīng)該包含其它的失敗情況)。
- 計(jì)算整個(gè)獲取鎖的過程總共消耗了多長時(shí)間诫给,計(jì)算方法是用當(dāng)前時(shí)間減去第1步記錄的時(shí)間香拉。如果客戶端從大多數(shù)Redis節(jié)點(diǎn)(>= N/2+1)成功獲取到了鎖,并且獲取鎖總共消耗的時(shí)間沒有超過鎖的有效時(shí)間(lock validity time)中狂,那么這時(shí)客戶端才認(rèn)為最終獲取鎖成功凫碌;否則,認(rèn)為最終獲取鎖失敗胃榕。
- 如果最終獲取鎖成功了盛险,那么這個(gè)鎖的有效時(shí)間應(yīng)該重新計(jì)算,它等于最初的鎖的有效時(shí)間減去第3步計(jì)算出來的獲取鎖消耗的時(shí)間。
- 如果最終獲取鎖失敗了(可能由于獲取到鎖的Redis節(jié)點(diǎn)個(gè)數(shù)少于N/2+1苦掘,或者整個(gè)獲取鎖的過程消耗的時(shí)間超過了鎖的最初有效時(shí)間)换帜,那么客戶端應(yīng)該立即向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖的操作(即前面介紹的Redis Lua腳本)。
當(dāng)然鹤啡,上面描述的只是獲取鎖的過程惯驼,而釋放鎖的過程比較簡單:客戶端向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖的操作,不管這些節(jié)點(diǎn)當(dāng)時(shí)在獲取鎖的時(shí)候成功與否揉忘。
由于N個(gè)Redis節(jié)點(diǎn)中的大多數(shù)能正常工作就能保證Redlock正常工作跳座,因此理論上它的可用性更高。我們前面討論的單Redis節(jié)點(diǎn)的分布式鎖在failover的時(shí)候鎖失效的問題泣矛,在Redlock中不存在了疲眷,但如果有節(jié)點(diǎn)發(fā)生崩潰重啟,還是會對鎖的安全性有影響的您朽。具體的影響程度跟Redis對數(shù)據(jù)的持久化程度有關(guān)狂丝。
假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A, B, C, D, E。設(shè)想發(fā)生了如下的事件序列: - 客戶端1成功鎖住了A, B, C哗总,獲取鎖成功(但D和E沒有鎖准秆铡)。
- 節(jié)點(diǎn)C崩潰重啟了讯屈,但客戶端1在C上加的鎖沒有持久化下來蛋哭,丟失了。
- 節(jié)點(diǎn)C重啟后涮母,客戶端2鎖住了C, D, E谆趾,獲取鎖成功。
這樣叛本,客戶端1和客戶端2同時(shí)獲得了鎖(針對同一資源)沪蓬。
在默認(rèn)情況下,Redis的AOF持久化方式是每秒寫一次磁盤(即執(zhí)行fsync)来候,因此最壞情況下可能丟失1秒的數(shù)據(jù)跷叉。為了盡可能不丟數(shù)據(jù),Redis允許設(shè)置成每次修改數(shù)據(jù)都進(jìn)行fsync营搅,但這會降低性能云挟。當(dāng)然,即使執(zhí)行了fsync也仍然有可能丟失數(shù)據(jù)(這取決于系統(tǒng)而不是Redis的實(shí)現(xiàn))转质。所以植锉,上面分析的由于節(jié)點(diǎn)重啟引發(fā)的鎖失效問題,總是有可能出現(xiàn)的峭拘。為了應(yīng)對這一問題,antirez又提出了延遲重啟(delayed restarts)的概念。也就是說鸡挠,一個(gè)節(jié)點(diǎn)崩潰后辉饱,先不立即重啟它,而是等待一段時(shí)間再重啟拣展,這段時(shí)間應(yīng)該大于鎖的有效時(shí)間(lock validity time)彭沼。這樣的話,這個(gè)節(jié)點(diǎn)在重啟前所參與的鎖都會過期备埃,它在重啟后就不會對現(xiàn)有的鎖造成影響姓惑。
關(guān)于Redlock還有一點(diǎn)細(xì)節(jié)值得拿出來分析一下:在最后釋放鎖的時(shí)候,antirez在算法描述中特別強(qiáng)調(diào)按脚,客戶端應(yīng)該向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖的操作于毙。也就是說,即使當(dāng)時(shí)向某個(gè)節(jié)點(diǎn)獲取鎖沒有成功辅搬,在釋放鎖的時(shí)候也不應(yīng)該漏掉這個(gè)節(jié)點(diǎn)唯沮。這是為什么呢?設(shè)想這樣一種情況堪遂,客戶端發(fā)給某個(gè)Redis節(jié)點(diǎn)的獲取鎖的請求成功到達(dá)了該Redis節(jié)點(diǎn)介蛉,這個(gè)節(jié)點(diǎn)也成功執(zhí)行了SET操作,但是它返回給客戶端的響應(yīng)包卻丟失了溶褪。這在客戶端看來币旧,獲取鎖的請求由于超時(shí)而失敗了,但在Redis這邊看來猿妈,加鎖已經(jīng)成功了吹菱。因此,釋放鎖的時(shí)候于游,客戶端也應(yīng)該對當(dāng)時(shí)獲取鎖失敗的那些Redis節(jié)點(diǎn)同樣發(fā)起請求毁葱。實(shí)際上,這種情況在異步通信模型中是有可能發(fā)生的:客戶端向服務(wù)器通信是正常的贰剥,但反方向卻是有問題的倾剿。
【其它疑問】
前面在討論單Redis節(jié)點(diǎn)的分布式鎖的時(shí)候,最后我們提出了一個(gè)疑問蚌成,如果客戶端長期阻塞導(dǎo)致鎖過期前痘,那么它接下來訪問共享資源就不安全了(沒有了鎖的保護(hù))。這個(gè)問題在Redlock中是否有所改善呢担忧?顯然芹缔,這樣的問題在Redlock中是依然存在的。
另外瓶盛,在算法第4步成功獲取了鎖之后最欠,如果由于獲取鎖的過程消耗了較長時(shí)間示罗,重新計(jì)算出來的剩余的鎖有效時(shí)間很短了,那么我們還來得及去完成共享資源訪問嗎芝硬?如果我們認(rèn)為太短蚜点,是不是應(yīng)該立即進(jìn)行鎖的釋放操作?那到底多短才算呢拌阴?又是一個(gè)選擇難題绍绘。
Martin的分析
Martin Kleppmann在2016-02-08這一天發(fā)表了一篇blog,名字叫”How to do distributed locking “迟赃,地址如下:
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
Martin在這篇文章中談及了分布式系統(tǒng)的很多基礎(chǔ)性的問題(特別是分布式計(jì)算的異步模型)陪拘,對分布式系統(tǒng)的從業(yè)者來說非常值得一讀。這篇文章大體可以分為兩大部分:
前半部分纤壁,與Redlock無關(guān)左刽。Martin指出,即使我們擁有一個(gè)完美實(shí)現(xiàn)的分布式鎖(帶自動過期功能)摄乒,在沒有共享資源參與進(jìn)來提供某種fencing機(jī)制的前提下悠反,我們?nèi)匀徊豢赡塬@得足夠的安全性。
后半部分馍佑,是對Redlock本身的批評斋否。Martin指出,由于Redlock本質(zhì)上是建立在一個(gè)同步模型之上拭荤,對系統(tǒng)的記時(shí)假設(shè)(timing assumption)有很強(qiáng)的要求茵臭,因此本身的安全性是不夠的。
首先我們討論一下前半部分的關(guān)鍵點(diǎn)舅世。Martin給出了下面這樣一份時(shí)序圖:
在上面的時(shí)序圖中旦委,假設(shè)鎖服務(wù)本身是沒有問題的,它總是能保證任一時(shí)刻最多只有一個(gè)客戶端獲得鎖雏亚。上圖中出現(xiàn)的lease這個(gè)詞可以暫且認(rèn)為就等同于一個(gè)帶有自動過期功能的鎖缨硝。客戶端1在獲得鎖之后發(fā)生了很長時(shí)間的GC pause罢低,在此期間查辩,它獲得的鎖過期了,而客戶端2獲得了鎖网持。當(dāng)客戶端1從GC pause中恢復(fù)過來的時(shí)候宜岛,它不知道自己持有的鎖已經(jīng)過期了,它依然向共享資源(上圖中是一個(gè)存儲服務(wù))發(fā)起了寫數(shù)據(jù)請求功舀,而這時(shí)鎖實(shí)際上被客戶端2持有萍倡,因此兩個(gè)客戶端的寫請求就有可能沖突(鎖的互斥作用失效了)。
初看上去辟汰,有人可能會說列敲,既然客戶端1從GC pause中恢復(fù)過來以后不知道自己持有的鎖已經(jīng)過期了阱佛,那么它可以在訪問共享資源之前先判斷一下鎖是否過期。但仔細(xì)想想戴而,這絲毫也沒有幫助瘫絮。因?yàn)镚C pause可能發(fā)生在任意時(shí)刻,也許恰好在判斷完之后填硕。
也有人會說,如果客戶端使用沒有GC的語言來實(shí)現(xiàn)鹿鳖,是不是就沒有這個(gè)問題呢扁眯?Martin指出,系統(tǒng)環(huán)境太復(fù)雜翅帜,仍然有很多原因?qū)е逻M(jìn)程的pause姻檀,比如虛存造成的缺頁故障(page fault),再比如CPU資源的競爭涝滴。即使不考慮進(jìn)程pause的情況绣版,網(wǎng)絡(luò)延遲也仍然會造成類似的結(jié)果。
總結(jié)起來就是說歼疮,即使鎖服務(wù)本身是沒有問題的杂抽,而僅僅是客戶端有長時(shí)間的pause或網(wǎng)絡(luò)延遲,仍然會造成兩個(gè)客戶端同時(shí)訪問共享資源的沖突情況發(fā)生韩脏。而這種情況其實(shí)就是我們在前面已經(jīng)提出來的“客戶端長期阻塞導(dǎo)致鎖過期”的那個(gè)疑問缩麸。
那怎么解決這個(gè)問題呢?Martin給出了一種方法赡矢,稱為fencing token杭朱。fencing token是一個(gè)單調(diào)遞增的數(shù)字,當(dāng)客戶端成功獲取鎖的時(shí)候它隨同鎖一起返回給客戶端吹散。而客戶端訪問共享資源的時(shí)候帶著這個(gè)fencing token,這樣提供共享資源的服務(wù)就能根據(jù)它進(jìn)行檢查,拒絕掉延遲到來的訪問請求(避免了沖突)奸绷。如下圖:
在上圖中酪呻,客戶端1先獲取到的鎖,因此有一個(gè)較小的fencing token袭景,等于33唁桩,而客戶端2后獲取到的鎖,有一個(gè)較大的fencing token耸棒,等于34荒澡。客戶端1從GC pause中恢復(fù)過來之后与殃,依然是向存儲服務(wù)發(fā)送訪問請求单山,但是帶了fencing token = 33碍现。存儲服務(wù)發(fā)現(xiàn)它之前已經(jīng)處理過34的請求,所以會拒絕掉這次33的請求米奸。這樣就避免了沖突昼接。
現(xiàn)在我們再討論一下Martin的文章的后半部分。
Martin在文中構(gòu)造了一些事件序列悴晰,能夠讓Redlock失效(兩個(gè)客戶端同時(shí)持有鎖)慢睡。為了說明Redlock對系統(tǒng)記時(shí)(timing)的過分依賴,他首先給出了下面的一個(gè)例子(還是假設(shè)有5個(gè)Redis節(jié)點(diǎn)A, B, C, D, E):
- 客戶端1從Redis節(jié)點(diǎn)A, B, C成功獲取了鎖(多數(shù)節(jié)點(diǎn))铡溪。由于網(wǎng)絡(luò)問題漂辐,與D和E通信失敗。
- 節(jié)點(diǎn)C上的時(shí)鐘發(fā)生了向前跳躍棕硫,導(dǎo)致它上面維護(hù)的鎖快速過期髓涯。
- 客戶端2從Redis節(jié)點(diǎn)C, D, E成功獲取了同一個(gè)資源的鎖(多數(shù)節(jié)點(diǎn))。
- 客戶端1和客戶端2現(xiàn)在都認(rèn)為自己持有了鎖哈扮。
上面這種情況之所以有可能發(fā)生纬纪,本質(zhì)上是因?yàn)镽edlock的安全性(safety property)對系統(tǒng)的時(shí)鐘有比較強(qiáng)的依賴,一旦系統(tǒng)的時(shí)鐘變得不準(zhǔn)確滑肉,算法的安全性也就保證不了了包各。Martin在這里其實(shí)是要指出分布式算法研究中的一些基礎(chǔ)性問題,或者說一些常識問題赦邻,即好的分布式算法應(yīng)該基于異步模型(asynchronous model)髓棋,算法的安全性不應(yīng)該依賴于任何記時(shí)假設(shè)(timing assumption)。在異步模型中:進(jìn)程可能pause任意長的時(shí)間惶洲,消息可能在網(wǎng)絡(luò)中延遲任意長的時(shí)間按声,甚至丟失,系統(tǒng)時(shí)鐘也可能以任意方式出錯(cuò)恬吕。一個(gè)好的分布式算法签则,這些因素不應(yīng)該影響它的安全性(safety property),只可能影響到它的活性(liveness property)铐料,也就是說渐裂,即使在非常極端的情況下(比如系統(tǒng)時(shí)鐘嚴(yán)重錯(cuò)誤),算法頂多是不能在有限的時(shí)間內(nèi)給出結(jié)果而已钠惩,而不應(yīng)該給出錯(cuò)誤的結(jié)果柒凉。這樣的算法在現(xiàn)實(shí)中是存在的,像比較著名的Paxos篓跛,或Raft膝捞。但顯然按這個(gè)標(biāo)準(zhǔn)的話,Redlock的安全性級別是達(dá)不到的愧沟。
隨后蔬咬,Martin覺得前面這個(gè)時(shí)鐘跳躍的例子還不夠鲤遥,又給出了一個(gè)由客戶端GC pause引發(fā)Redlock失效的例子。如下:
- 客戶端1向Redis節(jié)點(diǎn)A, B, C, D, E發(fā)起鎖請求林艘。
- 各個(gè)Redis節(jié)點(diǎn)已經(jīng)把請求結(jié)果返回給了客戶端1盖奈,但客戶端1在收到請求結(jié)果之前進(jìn)入了長時(shí)間的GC pause。
- 在所有的Redis節(jié)點(diǎn)上狐援,鎖過期了钢坦。
- 客戶端2在A, B, C, D, E上獲取到了鎖。
- 客戶端1從GC pause從恢復(fù)啥酱,收到了前面第2步來自各個(gè)Redis節(jié)點(diǎn)的請求結(jié)果场钉。客戶端1認(rèn)為自己成功獲取到了鎖懈涛。
- 客戶端1和客戶端2現(xiàn)在都認(rèn)為自己持有了鎖。
Martin給出的這個(gè)例子其實(shí)有點(diǎn)小問題泳猬。在Redlock算法中批钠,客戶端在完成向各個(gè)Redis節(jié)點(diǎn)的獲取鎖的請求之后,會計(jì)算這個(gè)過程消耗的時(shí)間得封,然后檢查是不是超過了鎖的有效時(shí)間(lock validity time)埋心。也就是上面的例子中第5步,客戶端1從GC pause中恢復(fù)過來以后忙上,它會通過這個(gè)檢查發(fā)現(xiàn)鎖已經(jīng)過期了拷呆,不會再認(rèn)為自己成功獲取到鎖了。隨后antirez在他的反駁文章中就指出來了這個(gè)問題疫粥,但Martin認(rèn)為這個(gè)細(xì)節(jié)對Redlock整體的安全性沒有本質(zhì)的影響茬斧。
拋開這個(gè)細(xì)節(jié),我們可以分析一下Martin舉這個(gè)例子的意圖在哪梗逮。初看起來项秉,這個(gè)例子跟文章前半部分分析通用的分布式鎖時(shí)給出的GC pause的時(shí)序圖是基本一樣的,只不過那里的GC pause發(fā)生在客戶端1獲得了鎖之后慷彤,而這里的GC pause發(fā)生在客戶端1獲得鎖之前娄蔼。但兩個(gè)例子的側(cè)重點(diǎn)不太一樣。Martin構(gòu)造這里的這個(gè)例子底哗,是為了強(qiáng)調(diào)在一個(gè)分布式的異步環(huán)境下岁诉,長時(shí)間的GC pause或消息延遲(上面這個(gè)例子中,把GC pause換成Redis節(jié)點(diǎn)和客戶端1之間的消息延遲跋选,邏輯不變)涕癣,會讓客戶端獲得一個(gè)已經(jīng)過期的鎖。從客戶端1的角度看野建,Redlock的安全性被打破了属划,因?yàn)榭蛻舳?收到鎖的時(shí)候恬叹,這個(gè)鎖已經(jīng)失效了,而Redlock同時(shí)還把這個(gè)鎖分配給了客戶端2同眯。換句話說绽昼,Redis服務(wù)器在把鎖分發(fā)給客戶端的途中,鎖就過期了须蜗,但又沒有有效的機(jī)制讓客戶端明確知道這個(gè)問題硅确。而在之前的那個(gè)例子中,客戶端1收到鎖的時(shí)候鎖還是有效的明肮,鎖服務(wù)本身的安全性可以認(rèn)為沒有被打破菱农,后面雖然也出了問題,但問題是出在客戶端1和共享資源服務(wù)器之間的交互上柿估。
在Martin的這篇文章中循未,還有一個(gè)很有見地的觀點(diǎn),就是對鎖的用途的區(qū)分秫舌。他把鎖的用途分為兩種:
為了效率(efficiency)的妖,協(xié)調(diào)各個(gè)客戶端避免做重復(fù)的工作。即使鎖偶爾失效了足陨,只是可能把某些操作多做一遍而已嫂粟,不會產(chǎn)生其它的不良后果。比如重復(fù)發(fā)送了一封同樣的email墨缘。
為了正確性(correctness)星虹。在任何情況下都不允許鎖失效的情況發(fā)生,因?yàn)橐坏┌l(fā)生镊讼,就可能意味著數(shù)據(jù)不一致(inconsistency)宽涌,數(shù)據(jù)丟失,文件損壞蝶棋,或者其它嚴(yán)重的問題护糖。
最后,Martin得出了如下的結(jié)論:
如果是為了效率(efficiency)而使用分布式鎖嚼松,允許鎖的偶爾失效嫡良,那么使用單Redis節(jié)點(diǎn)的鎖方案就足夠了,簡單而且效率高献酗。Redlock則是個(gè)過重的實(shí)現(xiàn)(heavyweight)寝受。
如果是為了正確性(correctness)在很嚴(yán)肅的場合使用分布式鎖,那么不要使用Redlock罕偎。它不是建立在異步模型上的一個(gè)足夠強(qiáng)的算法很澄,它對于系統(tǒng)模型的假設(shè)中包含很多危險(xiǎn)的成分(對于timing)。而且,它沒有一個(gè)機(jī)制能夠提供fencing token甩苛。那應(yīng)該使用什么技術(shù)呢蹂楣?Martin認(rèn)為,應(yīng)該考慮類似Zookeeper的方案讯蒲,或者支持事務(wù)的數(shù)據(jù)庫痊土。
Martin對Redlock算法的形容是:
neither fish nor fowl (非驢非馬)