Redis作者和分布式大神打架,最大的贏家竟然是吃瓜網(wǎng)友
背景鋪墊
面試的時(shí)候,不管你的簡歷寫沒寫 Redis,它基本上是一個(gè)繞不過的話題。

為了引出本文要討論的關(guān)于 Redlock 的神仙打架的問題,我們就得先通過一個(gè)面試連環(huán)炮:
1.Redis 做分布式鎖的時(shí)候有需要注意的問題?
2.如果是 Redis 是單點(diǎn)部署的,會(huì)帶來什么問題?
3.那你準(zhǔn)備怎么解決單點(diǎn)問題呢?
4.集群模式下,比如主從模式,有沒有什么問題呢?
5.你知道 Redis 是怎么解決集群模式也不靠譜的問題的嗎?
6.那你簡單的介紹一下 Redlock 吧?
7.你覺得 Redlock 有什么問題呢?
很明顯,上面是一個(gè)常規(guī)的面試連環(huán)套路題。中間還可以插入很多其他的 Redis 的考察點(diǎn),我這里就不做擴(kuò)展了。
單點(diǎn)的 Redis 做分布式鎖不靠譜,導(dǎo)致了基于 Redis 集群模式的分布式鎖解決方案的出現(xiàn)。
基于 Redis 集群模式的分布式鎖解決方案還是不靠譜,Redis 的作者提出了 Redlock 的解決方案。
Redis 作者提出的 Redlock 的解決方案,另一位分布式系統(tǒng)的大神覺得它不靠譜,于是他們之間開始了 battle。
基于這場(chǎng) battle,又引發(fā)了更多的討論。
這場(chǎng) battle 難分伯仲,沒有最后的贏家。如果一定要選出誰是最大的贏家的話,那一定是吃瓜網(wǎng)友。因?yàn)閷?duì)于吃瓜網(wǎng)友來說(比如我),可以從兩位大神的交鋒中學(xué)習(xí)到很多東西。
讓你深刻的體會(huì)到:看起來那么無懈可擊的想法,細(xì)細(xì)推敲之下,并不是那么天衣無縫。
所以本文就按照下面的五個(gè)模塊展開講述。

先來一波勸退:本文近1.2w字,謹(jǐn)慎觀看。看不下去不要緊,點(diǎn)個(gè)贊就是對(duì)于我最大的鼓勵(lì)。奧利給!
單點(diǎn)Redis
按照我的經(jīng)驗(yàn),當(dāng)面試聊到 Redis 的時(shí)候,百分之 90 的朋友都會(huì)說**:Redis在我們的項(xiàng)目中是用來做熱點(diǎn)數(shù)據(jù)緩存的**。
然后百分之百的面試官都會(huì)問:
Redis除了拿來做緩存,你還見過基于Redis的什么用法?
接下來百分之 80 的朋友都會(huì)說到:我們還用 Redis 做過分布式鎖。
(當(dāng)然, Redis 除了緩存、分布式鎖之外還有非常非常多的奇技淫巧,不是本文重點(diǎn),大家有興趣的可以自己去了解一下。)
那么面試官就會(huì)接著說:
那你給我描述(或者寫一下偽代碼)基于Redis的加鎖和釋放鎖的細(xì)節(jié)吧。
注意面試官這里說的是加鎖和釋放鎖的細(xì)節(jié),魔鬼都在細(xì)節(jié)里。
問這個(gè)問題面試官無非是想要聽到下面幾個(gè)關(guān)鍵點(diǎn):
關(guān)鍵點(diǎn)一:原子命令加鎖。因?yàn)橛械?ldquo;年久失修”的文章中對(duì)于 Redis 的加鎖操作是先set key,再設(shè)置 key 的過期時(shí)間。這樣寫的根本原因是在早期的 Redis 版本中并不支持原子命令加鎖的操作。不是原子操作會(huì)帶來什么問題,就不用我說了吧?如果你不知道,你先回去等通知吧。
而在 2.6.12 版本后,可以通過向 Redis 發(fā)送下面的命令,實(shí)現(xiàn)原子性的加鎖操作:
SET key random_value NX PX 30000
關(guān)鍵點(diǎn)二:設(shè)置值的時(shí)候,放的是random_value。而不是你隨便扔個(gè)“OK”進(jìn)去。
先解釋一下上面的命令中的幾個(gè)參數(shù)的含義:
random_value:是由客戶端生成的一個(gè)隨機(jī)字符串,它要保證在足夠長的一段時(shí)間內(nèi)在所有客戶端的所有獲取鎖的請(qǐng)求中都是唯一的。
NX:表示只有當(dāng)要設(shè)置的 key 值不存在的時(shí)候才能 set 成功。這保證了只有第一個(gè)請(qǐng)求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖。
PX 30000:表示這個(gè)鎖有一個(gè) 30 秒的自動(dòng)過期時(shí)間。當(dāng)然,這里 30 秒只是一個(gè)例子,客戶端可以選擇合適的過期時(shí)間。
再解釋一下為什么 value 需要設(shè)置為一個(gè)隨機(jī)字符串。這也是第三個(gè)關(guān)鍵點(diǎn)。
關(guān)鍵點(diǎn)三:value 的值設(shè)置為隨機(jī)數(shù)主要是為了更安全的釋放鎖,釋放鎖的時(shí)候需要檢查 key 是否存在,且 key 對(duì)應(yīng)的值是否和我指定的值一樣,是一樣的才能釋放鎖。所以可以看到這里有獲取、判斷、刪除三個(gè)操作,為了保障原子性,我們需要用 lua 腳本。
(基本上能答到這幾個(gè)關(guān)鍵點(diǎn),面試官也就會(huì)進(jìn)入下一個(gè)問題了。常規(guī)熱身送分題呀,朋友們,得記住了。)
集群模式
面試官就會(huì)接著問了:
經(jīng)過剛剛的討論,我們已經(jīng)有較好的方法獲取鎖和釋放鎖。基于Redis單實(shí)例,假設(shè)這個(gè)單實(shí)例總是可用,這種方法已經(jīng)足夠安全。如果這個(gè)Redis節(jié)點(diǎn)掛掉了呢?
到這個(gè)問題其實(shí)可以直接聊到 Redlock 了。但是你別慌啊,為了展示你豐富的知識(shí)儲(chǔ)備(瘋狂的刷題準(zhǔn)備),你得先自己聊一聊 Redis 的集群,你可以這樣去說:
為了避免節(jié)點(diǎn)掛掉導(dǎo)致的問題,我們可以采用Redis集群的方法來實(shí)現(xiàn)Redis的高可用。
Redis集群方式共有三種:主從模式,哨兵模式,cluster(集群)模式
其中主從模式會(huì)保證數(shù)據(jù)在從節(jié)點(diǎn)還有一份,但是主節(jié)點(diǎn)掛了之后,需要手動(dòng)把從節(jié)點(diǎn)切換為主節(jié)點(diǎn)。它非常簡單,但是在實(shí)際的生產(chǎn)環(huán)境中是很少使用的。
哨兵模式就是主從模式的升級(jí)版,該模式下會(huì)對(duì)響應(yīng)異常的主節(jié)點(diǎn)進(jìn)行主觀下線或者客觀下線的操作,并進(jìn)行主從切換。它可以保證高可用。
cluster (集群)模式保證的是高并發(fā),整個(gè)集群分擔(dān)所有數(shù)據(jù),不同的 key 會(huì)放到不同的 Redis 中。每個(gè) Redis 對(duì)應(yīng)一部分的槽。
(上面三種模式也是面試重點(diǎn),可以說很多道道出來,由于不是本文重點(diǎn)就不詳細(xì)描述了。主要表達(dá)的意思是你得在面試的時(shí)候遇到相關(guān)問題,需要展示自己是知道這些東西的,都是面試的套路。)
在上面描述的集群模式下還是會(huì)出現(xiàn)一個(gè)問題,由于節(jié)點(diǎn)之間是采用異步通信的方式。如果剛剛在 Master 節(jié)點(diǎn)上加了鎖,但是數(shù)據(jù)還沒被同步到 Salve。這時(shí) Master 節(jié)點(diǎn)掛了,它上面的鎖就沒了,等新的 Master 出來后(主從模式的手動(dòng)切換或者哨兵模式的一次 failover 的過程),就可以再次獲取同樣的鎖,出現(xiàn)一把鎖被拿到了兩次的場(chǎng)景。
鎖都被拿了兩次了,也就不滿足安全性了。一個(gè)安全的鎖,不管是不是分布式的,在任意一個(gè)時(shí)刻,都只有一個(gè)客戶端持有。
Redlock簡介
為了解決上面的問題,Redis 的作者提出了名為 Redlock 的算法。
在 Redis 的分布式環(huán)境中,我們假設(shè)有 N 個(gè) Redis Master。這些節(jié)點(diǎn)完全互相獨(dú)立,不存在主從復(fù)制或者其他集群協(xié)調(diào)機(jī)制。
前面已經(jīng)描述了在單點(diǎn) Redis 下,怎么安全地獲取和釋放鎖,我們確保將在 N 個(gè)實(shí)例上使用此方法獲取和釋放鎖。
在下面的示例中,我們假設(shè)有 5 個(gè)完全獨(dú)立的 Redis Master 節(jié)點(diǎn),他們分別運(yùn)行在 5 臺(tái)服務(wù)器中,可以保證他們不會(huì)同時(shí)宕機(jī)。
從官網(wǎng)上我們可以知道,一個(gè)客戶端如果要獲得鎖,必須經(jīng)過下面的五個(gè)步驟:
步驟描述來源:http://redis.cn/topics/distlock.html
1.獲取當(dāng)前 Unix 時(shí)間,以毫秒為單位。
2.依次嘗試從 N 個(gè)實(shí)例,使用相同的 key 和隨機(jī)值獲取鎖。在步驟 2,當(dāng)向 Redis 設(shè)置鎖時(shí),客戶端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。例如你的鎖自動(dòng)失效時(shí)間為 10 秒,則超時(shí)時(shí)間應(yīng)該在 5-50 毫秒之間。這樣可以避免服務(wù)器端 Redis 已經(jīng)掛掉的情況下,客戶端還在死死地等待響應(yīng)結(jié)果。如果服務(wù)器端沒有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試另外一個(gè) Redis 實(shí)例。
3.客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(步驟 1 記錄的時(shí)間)就得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(這里是 3 個(gè)節(jié)點(diǎn))的 Redis 節(jié)點(diǎn)都取到鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。
4.如果取到了鎖,key 的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟 3 計(jì)算的結(jié)果)。
5.如果因?yàn)槟承┰颍@取鎖失敗(沒有在至少 N/2+1 個(gè)Redis實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過了有效時(shí)間),客戶端應(yīng)該在所有的 Redis 實(shí)例上進(jìn)行解鎖(即便某些 Redis 實(shí)例根本就沒有加鎖成功)。
通過上面的步驟我們可以知道,只要大多數(shù)的節(jié)點(diǎn)可以正常工作,就可以保證 Redlock 的正常工作。這樣就可以解決前面單點(diǎn) Redis 的情況下我們討論的節(jié)點(diǎn)掛掉,由于異步通信,導(dǎo)致鎖失效的問題。
但是,還是不能解決故障重啟后帶來的鎖的安全性的問題。你想一下下面這個(gè)場(chǎng)景:
我們一共有 A、B、C 這三個(gè)節(jié)點(diǎn)。
1.客戶端 1 在 A,B 上加鎖成功。C 上加鎖失敗。
2.這時(shí)節(jié)點(diǎn) B 崩潰重啟了,但是由于持久化策略導(dǎo)致客戶端 1 在 B 上的鎖沒有持久化下來。 客戶端 2 發(fā)起申請(qǐng)同一把鎖的操作,在 B,C 上加鎖成功。
3.這個(gè)時(shí)候就又出現(xiàn)同一把鎖,同時(shí)被客戶端 1 和客戶端 2 所持有了。
(接下來又得說一說Redis的持久化策略了,全是知識(shí)點(diǎn)啊,朋友們)
比如,Redis 的 AOF 持久化方式默認(rèn)情況下是每秒寫一次磁盤,即 fsync 操作,因此最壞的情況下可能丟失 1 秒的數(shù)據(jù)。
當(dāng)然,你也可以設(shè)置成每次修改數(shù)據(jù)都進(jìn)行 fsync 操作(fsync=always),但這會(huì)嚴(yán)重降低 Redis 的性能,違反了它的設(shè)計(jì)理念。(我也沒見過這樣用的,可能還是見的太少了吧。)
而且,你以為執(zhí)行了 fsync 就不會(huì)丟失數(shù)據(jù)了?天真,真實(shí)的系統(tǒng)環(huán)境是復(fù)雜的,這都已經(jīng)脫離 Redis 的范疇了。上升到服務(wù)器、系統(tǒng)問題了。
所以,根據(jù)墨菲定律,上面舉的例子:由于節(jié)點(diǎn)重啟引發(fā)的鎖失效問題,總是有可能出現(xiàn)的。
為了解決這一問題,Redis 的作者又提出了延遲重啟(delayed restarts)的概念。

意思就是說,一個(gè)節(jié)點(diǎn)崩潰后,不要立即重啟它,而是等待一定的時(shí)間后再重啟。等待的時(shí)間應(yīng)該大于鎖的過期時(shí)間(TTL)。這樣做的目的是保證這個(gè)節(jié)點(diǎn)在重啟前所參與的鎖都過期。相當(dāng)于把以前的帳勾銷之后才能參與后面的加鎖操作。
但是有個(gè)問題就是:在等待的時(shí)間內(nèi),這個(gè)節(jié)點(diǎn)是不對(duì)外工作的。那么如果大多數(shù)節(jié)點(diǎn)都掛了,進(jìn)入了等待。就會(huì)導(dǎo)致系統(tǒng)的不可用,因?yàn)橄到y(tǒng)在TTL時(shí)間內(nèi)任何鎖都將無法加鎖成功。
Redlock 算法還有一個(gè)需要注意的點(diǎn)是它的釋放鎖操作。
釋放鎖的時(shí)候是要向所有節(jié)點(diǎn)發(fā)起釋放鎖的操作的。這樣做的目的是為了解決有可能在加鎖階段,這個(gè)節(jié)點(diǎn)收到加鎖請(qǐng)求了,也set成功了,但是由于返回給客戶端的響應(yīng)包丟了,導(dǎo)致客戶端以為沒有加鎖成功。所以,釋放鎖的時(shí)候要向所有節(jié)點(diǎn)發(fā)起釋放鎖的操作。
你可能覺得這不是常規(guī)操作嗎?
有的細(xì)節(jié)就是這樣,說出來后覺得不過如此,但是有可能自己就是想不到這個(gè)點(diǎn),導(dǎo)致問題的出現(xiàn),所以我們才會(huì)說:細(xì)節(jié),魔鬼都在細(xì)節(jié)里。
好了,簡介大概就說到這里,有興趣的朋友可以再去看看官網(wǎng),補(bǔ)充一下。
中文:http://redis.cn/topics/distlock.html英文:https://redis.io/topics/distlock
好了,經(jīng)過這么長,這么長的鋪墊,我們終于可以進(jìn)入到神仙打架環(huán)節(jié)。
神仙打架
神仙一:Redis 的作者 antirez 。有的朋友對(duì)英文名字不太敏感,所以后面我就叫他卷發(fā)哥吧。
神仙二:分布式領(lǐng)域?qū)<?Martin Kleppmann,我們叫他長發(fā)哥吧。
看完上面兩位神仙的照片,再看看我為了寫這篇文章又日漸稀少的頭發(fā),我忍不住哭出聲來。可能只有給我點(diǎn)贊,才能平復(fù)我的心情吧。
卷發(fā)哥在官網(wǎng)介紹 Redlock 頁面的最后寫到:如果你也是使用分布式系統(tǒng)的人員,你的觀點(diǎn)和意見非常重要,歡迎和我們討論。

于是,“求錘得錘”!這一錘,錘出了眾多的吃瓜網(wǎng)友,其中不乏在相關(guān)領(lǐng)域的專業(yè)人士。
長發(fā)哥出錘
故事得從 2016年2月8號(hào) 長發(fā)哥發(fā)布的一篇文章《How to do distributed locking》說起:
文章地址:http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
這一部分直接翻譯過來就是:
作為本書(《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》)研究的一部分,我在Redis網(wǎng)站上 看到了一種稱為Redlock的算法。該算法聲稱在Redis實(shí)現(xiàn)容錯(cuò)的分布式鎖(或更確切地說, 租約),并且該頁面要求來自分布式系統(tǒng)人員的反饋。這個(gè)算法讓我產(chǎn)生了一些思考,因此我花了一些時(shí)間寫了我的這篇文章。由于Redlock已經(jīng)有10多個(gè)獨(dú)立的實(shí)現(xiàn),而且我們不知道誰已經(jīng)在依賴此算法,因此我認(rèn)為值得公開分享我的筆記。我不會(huì)討論Redis的其他方面,其中一些已經(jīng)在其他地方受到了批評(píng) 。
你看這個(gè)文章,開頭就是火藥味十足:你說要反饋,那我就給你反饋。而且你這個(gè)東西有其他問題,我也就不說了。(其實(shí)作者在這篇文章中也說了,他很喜歡并且也在使用 Redis,只是他覺得這個(gè) Redlock 算法是不嚴(yán)謹(jǐn)?shù)模?/p>
長發(fā)哥主要圍繞了下面的這張圖進(jìn)行了展開:

要是一眼沒看明白,我再給你一個(gè)中文版的,來自長發(fā)哥于2017年出版的書《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》:

可以看到上面的圖片中提到了申請(qǐng)租約、租約到期的關(guān)鍵詞,租約其實(shí)就是可以理解為帶超時(shí)時(shí)間的鎖。
而在書中,這張圖片的下面寫的描述這樣的,你咂摸咂摸:

拿 HBase 舉例,其設(shè)計(jì)的目標(biāo)是確保存儲(chǔ)系統(tǒng)的文件一次只能由一個(gè)客戶端訪問,如果多個(gè)客戶端試圖同時(shí)寫入該文件,文件就會(huì)被破壞。那么上面的圖片解釋起來就是:
1.客戶端 1 先去申請(qǐng)鎖,并且成功獲取到鎖。之后客戶端進(jìn)行了長時(shí)間的 GC 導(dǎo)致了 STW 的情況。
2.在 STW 期間,客戶端 1 獲取的鎖的超時(shí)時(shí)間到了,鎖也就失效了。
3.由于客戶端 1 的鎖已經(jīng)過期失效了,所以客戶端 2 去申請(qǐng)鎖就可以成功獲得鎖。
4.客戶端 2 開始寫文件,并完成文件的寫入。
5.客戶端 1 從 STW 中恢復(fù)過來,他并不知道自己的鎖過期了,還是會(huì)繼續(xù)執(zhí)行文件寫入操作,導(dǎo)致客戶端 2 寫入的文件被破壞。而且可以看到,它沒有滿足鎖在任意時(shí)刻只有一個(gè)客戶端持有的原則,即沒有滿足互斥性。
書里面沒有明說,但是你品一品,這里的鎖服務(wù)難道不是在說 Redis?
有的朋友就會(huì)說了,那客戶端 1 寫入文件的時(shí)候,再判斷一下自己的鎖有沒有過期不就可以了嗎?
你可真是個(gè)小機(jī)靈鬼呢,那我問你,GC 可能是發(fā)生在任何時(shí)間的,萬一 GC 發(fā)生在判斷之后呢?
你繼續(xù)懟我,如果客戶端使用的是沒有 GC 的語言呢?
**GC 不是導(dǎo)致線程暫停的唯一原因啊,朋友們。**發(fā)生這種情況的原因有很多的,你看看長發(fā)哥書里舉的例子:

上面的內(nèi)容總結(jié)起來,就是就算鎖服務(wù)是正常的,但是由于鎖是有持有時(shí)間的,由于客戶端阻塞、長時(shí)間的 GC 或者網(wǎng)絡(luò)原因,導(dǎo)致共享資源被一個(gè)以上的客戶端同時(shí)訪問了。
其實(shí)上面長發(fā)哥在書里直接說了:這是不正確的實(shí)現(xiàn)。
你多品一品,上面的圖是不是有點(diǎn)像由于 Redis 鎖的過期時(shí)間設(shè)置的不合理,導(dǎo)致前一個(gè)任務(wù)還沒執(zhí)行完成,但是鎖的時(shí)間到期了,后一個(gè)任務(wù)也申請(qǐng)到了鎖。
對(duì)于這種場(chǎng)景,Redission 其實(shí)有自己的看門狗機(jī)制。但是不在這次 Redlock 的討論范圍內(nèi),所以這里就不描述了。
長發(fā)哥提出的解決方案是什么呢?
他稱為:fencing token。
長發(fā)哥認(rèn)為使用鎖和租約機(jī)制來保護(hù)資源的并發(fā)訪問時(shí),必須確保因?yàn)楫惓T颍瑢?dǎo)致鎖過期的那個(gè)節(jié)點(diǎn)不能影響其他正常的部分,要實(shí)現(xiàn)這一目標(biāo),可以采用一直相當(dāng)簡單的 fencing(柵欄)。
假設(shè)每次鎖服務(wù)在授予鎖或者租約時(shí),還會(huì)同時(shí)返回一個(gè) fencing 令牌,該令牌每次授予都會(huì)遞增。
然后,要求客戶端每次向存儲(chǔ)系統(tǒng)發(fā)送寫請(qǐng)求時(shí),都必須包含所持有的 fencing 令牌。存儲(chǔ)系統(tǒng)需要對(duì)令牌進(jìn)行校驗(yàn),發(fā)現(xiàn)如果已經(jīng)處理過更高令牌的請(qǐng)求,則拒絕執(zhí)行該請(qǐng)求。
比如下面的圖片:

1.客戶端 1 獲得一個(gè)具有超時(shí)時(shí)間的鎖的同時(shí)得到了令牌號(hào) 33,但隨后陷入了一個(gè)長時(shí)間的暫停直到鎖到期。
2.這時(shí)客戶端2已經(jīng)獲得了鎖和令牌號(hào) 34 ,然后發(fā)送寫請(qǐng)求(以及令牌號(hào) 34 )到存儲(chǔ)服務(wù)。
3.接下來客戶端 1 恢復(fù)過來,并以令牌號(hào) 33 來嘗試寫入,存儲(chǔ)服務(wù)器由于記錄了最近已經(jīng)完成了更高令牌號(hào)(34 ),因此拒絕令牌號(hào) 33 的寫請(qǐng)求。
這種版本號(hào)的機(jī)制,讓我不禁想起了 Zookeeper。當(dāng)使用 ZK 做鎖服務(wù)時(shí),可以用事務(wù)標(biāo)識(shí) zxid 或節(jié)點(diǎn)版本 cversion 來充當(dāng) fencing 令牌,這兩個(gè)都可以滿足單調(diào)遞增的要求。
在長發(fā)哥的這種機(jī)制中,實(shí)際上就是要求資源本身必須主動(dòng)檢查請(qǐng)求所持令牌信息,如果發(fā)現(xiàn)已經(jīng)處理過更高令牌的請(qǐng)求,要拒絕持有低令牌的所有寫請(qǐng)求。
但是,不是所有的資源都是數(shù)據(jù)庫里面的數(shù)據(jù),我們可以通過版本號(hào)去支持額外的令牌檢查的,那么對(duì)于不支持額外的令牌檢查資源,我們也可以借助這種思想繞過這個(gè)限制,比如對(duì)于訪問文件存儲(chǔ)服務(wù)的情況,我們可以將令牌嵌入到文件名中。
總之,為了避免在鎖保護(hù)之外發(fā)生請(qǐng)求處理,需要進(jìn)行額外的檢查機(jī)制。
長發(fā)哥在書中也說到了:在服務(wù)端檢查令牌可能看起來有點(diǎn)復(fù)雜,但是這其實(shí)是推薦的正確的做法:系統(tǒng)服務(wù)不能假定所有的客戶端都表現(xiàn)的符合預(yù)期。從安全角度講,服務(wù)端必須防范這種來自客戶端的濫用。
這個(gè)就類似于我們作為后端開發(fā)人員,也不能相信來自前端或者其他接口過來的數(shù)據(jù),必須對(duì)其進(jìn)行校驗(yàn)。
到這里長發(fā)哥鋪墊完成了,開始轉(zhuǎn)頭指向 RedLock,他認(rèn)為 Redlock 是一個(gè)嚴(yán)重依賴系統(tǒng)時(shí)鐘的分布式鎖。

他舉了一個(gè)例子:
1.客戶端 1 從 Redis 節(jié)點(diǎn) A, B, C 成功獲取了鎖。由于網(wǎng)絡(luò)問題,無法訪問 D 和 E。
2.節(jié)點(diǎn) C 上的時(shí)鐘發(fā)生了向前跳躍,導(dǎo)致它上面維護(hù)的鎖過期了。
3.客戶端 2 從 Redis 節(jié)點(diǎn) C, D, E 成功獲取了同一個(gè)資源的鎖。由于網(wǎng)絡(luò)問題,無法訪問 A 和 B。 現(xiàn)在,客戶端 1 和客戶端 2 都認(rèn)為自己持有了鎖。
這樣的場(chǎng)景是可能出現(xiàn)的,因?yàn)?Redlock 嚴(yán)重依賴系統(tǒng)時(shí)鐘,所以一旦系統(tǒng)的時(shí)間變得不準(zhǔn)確了,那么該算法的安全性也就得不到保障了。
長發(fā)哥舉這個(gè)例子其實(shí)是為了輔佐他前面提出的觀點(diǎn):一個(gè)好的分布式算法應(yīng)該是基于異步模型的,算法的安全性不應(yīng)該依賴與任何記時(shí)假設(shè),就是不能把時(shí)間作為安全保障的。在異步模型中,程序暫停、消息在網(wǎng)絡(luò)中延遲甚至丟失、系統(tǒng)時(shí)間錯(cuò)誤這些因素都不應(yīng)該影響它的安全性,只能影響到它的活性。
用大白話說,就是在極其極端的情況下,分布式系統(tǒng)頂天了也就是在有限的時(shí)間內(nèi)不能給出結(jié)果而已,而不能給出一個(gè)錯(cuò)誤的結(jié)果。
這樣的算法實(shí)際上是存在的,比如 Paxos、Raft。很明顯,按照這個(gè)標(biāo)準(zhǔn), Redlock 的安全級(jí)別是不夠的。
而對(duì)于卷發(fā)哥提出的延遲啟動(dòng)方案,長發(fā)哥還是一棒子打死:你延遲啟動(dòng)咋的?延遲啟動(dòng)還不是依賴于合理準(zhǔn)確的時(shí)間度量。
可能是長發(fā)哥覺得舉這個(gè)時(shí)鐘跳躍的例子不夠好的,大家都可能認(rèn)為時(shí)鐘跳躍是不現(xiàn)實(shí)的,因?yàn)閷?duì)正確配置NTP就能擺正時(shí)鐘非常有信心。
在這種情況下,他舉了一個(gè)進(jìn)程暫停可能導(dǎo)致算法失敗的示例:

1.客戶端 1 向 Redis 節(jié)點(diǎn) A, B, C, D, E 發(fā)起鎖請(qǐng)求。
2.各個(gè) Redis 節(jié)點(diǎn)已經(jīng)把請(qǐng)求結(jié)果返回給了客戶端 1,但客戶端 1 在收到請(qǐng)求結(jié)果之前進(jìn)入了長時(shí)間的 GC 階段。
3.長時(shí)間的 GC,導(dǎo)致在所有的 Redis 節(jié)點(diǎn)上,鎖過期了。
4.客戶端 2 在 A, B, C, D, E 上申請(qǐng)并獲取到了鎖。
5.客戶端 1 從 GC 階段中恢復(fù),收到了前面第 2 步來自各個(gè) Redis 節(jié)點(diǎn)的請(qǐng)求結(jié)果。客戶端 1 認(rèn)為自己成功獲取到了鎖。
6.客戶端 1 和客戶端 2 現(xiàn)在都認(rèn)為自己持有了鎖。
其實(shí)只要十分清楚 Redlock 的加鎖過程,我們就知道,這種情況其實(shí)對(duì)于 Redlock 是沒有影響的,因?yàn)樵诘?5 步,客戶端 1 從 GC 階段中恢復(fù)過來以后,在 Redlock 算法中,(我們前面 Redlock 簡介的時(shí)候提到的第四步)如果取到了鎖,key 的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間。
所以客戶端1通過這個(gè)檢查發(fā)現(xiàn)鎖已經(jīng)過期了,不會(huì)再認(rèn)為自己成功獲取到鎖了。
而隨后卷發(fā)哥的回?fù)糁幸蔡岬搅诉@點(diǎn)。
但是,細(xì)細(xì)想來,我覺得長發(fā)哥的意圖不在于此。拋開上面的問題來講,他更想突出的是,一個(gè)鎖在客戶端拿到后,還沒使用就過期了,這是不好的。從客戶端的角度來看,就是這玩意不靠譜啊,你給我一把鎖,我還沒用呢,你就過期了?
除了上面說的這些點(diǎn)外,長發(fā)哥還提出了一個(gè)算是自己的經(jīng)驗(yàn)之談吧:
我們獲取鎖的用途是什么?
在他看來不外乎兩個(gè)方面,效率和正確性。他分別描述如下:

如果是為了效率,那么就是要協(xié)調(diào)各個(gè)客戶端,避免他們做重復(fù)的工作。這種場(chǎng)景下,即使鎖偶爾失效了,只是可能出現(xiàn)兩個(gè)客戶端完成了同樣的工作,其結(jié)果是成本略有增加(您最終向 AWS 支付的費(fèi)用比原本多5美分),或者帶來不便(例如,用戶最終兩次收到相同的電子郵件通知)。
如果是為了正確性,那么在任何情況下都不允許鎖失效的情況發(fā)生,因?yàn)橐坏┌l(fā)生,就可能意味著數(shù)據(jù)不一致,數(shù)據(jù)丟失,文件損壞,或者其它嚴(yán)重的問題。(比如個(gè)患者注射了兩倍的藥劑)
最后,長發(fā)哥得出的結(jié)論是:neither fish nor fowl(不倫不類)
對(duì)于提升效率的場(chǎng)景下,使用分布式鎖,允許鎖的偶爾失效,那么使用單 Redis 節(jié)點(diǎn)的鎖方案就足夠了,簡單而且效率高。用 Redlock 太重。
對(duì)于正確性要求高的場(chǎng)景下,它是依賴于時(shí)間的,不是一個(gè)足夠強(qiáng)的算法。Redlock并沒有保住正確性。

那應(yīng)該使用什么技術(shù)呢?
長發(fā)哥認(rèn)為,應(yīng)該考慮類似 Zookeeper 的方案,或者支持事務(wù)的數(shù)據(jù)庫。

卷發(fā)哥回?fù)?/h2>
長發(fā)哥發(fā)出《How to do distributed locking》這篇文章的第二天,卷發(fā)哥就進(jìn)行了回?fù)簦l(fā)布了名為《Is Redlock safe?》的文章。
文章地址:http://antirez.com/news/101
要說大佬不愧是大佬,卷發(fā)哥的回?fù)魲l理清楚,行文流暢。他總結(jié)后認(rèn)為長發(fā)哥覺得 Redlock 不安全主要分為兩個(gè)方面:

1.帶有自動(dòng)過期功能的分布式鎖,需要一種方法(fencing機(jī)制)來避免客戶端在過期時(shí)間后使用鎖時(shí)出現(xiàn)問題,從而對(duì)共享資源進(jìn)行真正的互斥保護(hù)。長發(fā)哥說Redlock沒有這種機(jī)制。
2.長發(fā)哥說,無論問題“1”如何解決,該算法本質(zhì)上都是不安全的,因?yàn)樗鼘?duì)系統(tǒng)模型進(jìn)行了記時(shí)假設(shè),而這些假設(shè)在實(shí)際系統(tǒng)中是無法保證的。
對(duì)于第一個(gè)點(diǎn),卷發(fā)哥列了5大點(diǎn)來反駁這個(gè)問題,其中一個(gè)重要的觀點(diǎn)是他認(rèn)為雖然 Redlock 沒有提供類似于fencing機(jī)制那樣的單調(diào)遞增的令牌,但是也有一個(gè)隨機(jī)串,把這個(gè)隨機(jī)串當(dāng)做token,也可以達(dá)到同樣的效果啊。當(dāng)需要和共享資源交互的時(shí)候,我們檢查一下這個(gè)token是否發(fā)生了變化,如果沒有再執(zhí)行“獲取-修改-寫回”的操作。

最終得出的結(jié)論是一個(gè)靈魂反問:既然在鎖失效的情況下已經(jīng)存在一種fencing機(jī)制能繼續(xù)保持資源的互斥訪問了,那為什么還要使用一個(gè)分布式鎖并且還要求它提供那么強(qiáng)的安全性保證呢?

然而第二個(gè)問題,對(duì)于網(wǎng)絡(luò)延遲或者 GC 暫停,我們前面分析過,對(duì) Redlock 的安全性并不會(huì)產(chǎn)生影響,說明卷發(fā)哥在設(shè)計(jì)的時(shí)候其實(shí)是考慮過時(shí)間因素帶來的問題的。
但是如果是長發(fā)哥提出的時(shí)鐘發(fā)生跳躍,很明顯,卷發(fā)哥知道如果時(shí)鐘發(fā)生跳躍, Redlock 的安全性就得不到保障,這是他的命門。
但是對(duì)于長發(fā)哥寫時(shí)鐘跳躍的時(shí)候提出的兩個(gè)例子:
1.運(yùn)維人員手動(dòng)修改了系統(tǒng)時(shí)鐘。
2.從NTP服務(wù)收到了一個(gè)大的時(shí)鐘更新事件。
卷發(fā)哥進(jìn)行了回?fù)簦?/p>
第一點(diǎn)這個(gè)運(yùn)維人員手動(dòng)修改時(shí)鐘,屬于人為因素,這個(gè)我也沒辦法啊,人家就是要搞你,怎么辦?加強(qiáng)管理,不要這樣做。
第二點(diǎn)從NTP服務(wù)收到一個(gè)大的時(shí)鐘更新,對(duì)于這個(gè)問題,需要通過運(yùn)維來保證。需要將大的時(shí)間更新到服務(wù)器的時(shí)候,應(yīng)當(dāng)采取少量多次的方式。多次修改,每次更新時(shí)間盡量小。
關(guān)于這個(gè)地方的爭論,就看你是信長發(fā)哥的時(shí)間一定會(huì)跳躍,還是信卷發(fā)哥的時(shí)間跳躍我們也是可以處理的。
關(guān)于時(shí)鐘跳躍,有一篇文章可以看看,也是這次神仙打架導(dǎo)致的產(chǎn)物:
https://jvns.ca/blog/2016/02/09/til-clock-skew-exists/
文章得出的最終結(jié)論是:時(shí)鐘跳躍是存在的。

其實(shí)我們大家應(yīng)該都經(jīng)歷過時(shí)鐘跳躍的情況,你還記得2016年的最后一天,當(dāng)時(shí)有個(gè)“閏秒”的概念嗎?導(dǎo)致2017年1月1日出現(xiàn)了07:59:60的奇觀。

打架的焦點(diǎn)
經(jīng)過這樣的一來一回,其實(shí)雙方打架的焦點(diǎn)就很明確了,就是大延遲對(duì)分布式鎖帶來的影響。
而對(duì)于大延遲給Redlock帶來的影響,就是長發(fā)哥分析的那樣,鎖到期了,業(yè)務(wù)還沒執(zhí)行完。卷發(fā)哥認(rèn)為這種影響不單單針對(duì) Redlock ,其他具有自動(dòng)釋放鎖的分布式鎖也是存在一樣的問題。

而關(guān)于大延遲的問題,我在某社交平臺(tái)上找到了兩位神仙的下面的對(duì)話:

卷發(fā)哥問:我想知道,在我發(fā)文回復(fù)之后,我們能否在一點(diǎn)上達(dá)成一致,就是大的消息延遲不會(huì)給Redlock的運(yùn)行造成損害。
長發(fā)哥答:對(duì)于客戶端和鎖服務(wù)器之間的消息延遲,我同意你的觀點(diǎn)。但客戶端和被訪問資源之間的延遲還是有問題的。
所以通過卷發(fā)哥的回?fù)粑恼潞湍成缃黄脚_(tái)的記錄,他是同意大的系統(tǒng)時(shí)鐘跳躍會(huì)造成 Redlock 失效的。在這一點(diǎn)上,他與長發(fā)哥的觀點(diǎn)的不同在于,他認(rèn)為在實(shí)際系統(tǒng)中是可以通過好的運(yùn)維方式避免大的時(shí)鐘跳躍的。
所以到這里,兩位神仙好像又達(dá)到了一個(gè)平衡,實(shí)現(xiàn)了爭論上的求同存異。
打架總結(jié)
作為一個(gè)互聯(lián)網(wǎng)行業(yè)的從業(yè)者,也是分布式系統(tǒng)的使用者,讀完他們的文章以及由此文章衍生出來的知識(shí)點(diǎn)后,受益良多,于是寫下此文作為學(xué)習(xí)總結(jié),也與大家分享。本文還有很多不足之處,還請(qǐng)各位海涵。
如同文章開篇說的,這場(chǎng)爭論沒有最后的贏家。很明顯卷發(fā)哥是沒有說服長發(fā)哥的,因?yàn)樵陂L發(fā)哥2017年出版的《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》一書中,專門有一小節(jié)的名稱叫做:不可靠的時(shí)鐘

其實(shí)在這場(chǎng)爭論的最后,長發(fā)哥對(duì)這場(chǎng)爭論進(jìn)行了一個(gè)非常感性的總結(jié),他說:
下面翻譯來自:https://www.jianshu.com/p/dd66bdd18a56
對(duì)我來說最重要的一點(diǎn)在于:我并不在乎在這場(chǎng)辯論中誰對(duì)誰錯(cuò) —— 我只關(guān)心從其他人的工作中學(xué)到的東西,以便我們能夠避免重蹈覆轍,并讓未來更加美好。前人已經(jīng)為我們創(chuàng)造出了許多偉大的成果:站在巨人的肩膀上,我們得以構(gòu)建更棒的軟件。
對(duì)于任何想法,務(wù)必要詳加檢驗(yàn),通過論證以及檢查它們是否經(jīng)得住別人的詳細(xì)審查。那是學(xué)習(xí)過程的一部分。但目標(biāo)應(yīng)該是為了獲得知識(shí),而不應(yīng)該是為了說服別人相信你自己是對(duì)的。有時(shí)候,那只不過意味著停下來,好好地想一想。
吃瓜網(wǎng)友的收獲
這里的吃瓜網(wǎng)友就是指我啦。
寫這篇文章我的收獲還是挺大的,首先我買了長發(fā)哥的《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》一書,讀了幾節(jié),發(fā)現(xiàn)這書是真的不錯(cuò),豆瓣評(píng)分9.6,推薦。
其次完成了這周的周更任務(wù),雖然寫的很艱難,從周六中午,寫到周日凌晨3點(diǎn)。。。
然后還吃到了另外的一個(gè)瓜,可謂是瓜中瓜。
這周五的時(shí)候 Redis 官網(wǎng)不是出現(xiàn)了短暫的宕機(jī)嗎,宕機(jī)其實(shí)也沒啥稀奇的,但是頁面上顯示的是連不上 Redis 。這就有點(diǎn)意思了。

我在寫這篇文章的時(shí)候,在卷發(fā)哥的某社交平臺(tái)上發(fā)現(xiàn)了這個(gè):

我關(guān)心的并不是 OOM,而是卷發(fā)哥居然讓 Redis 官網(wǎng)運(yùn)行在一臺(tái)一個(gè)月僅 5 美元,內(nèi)存只有 1G 的虛擬機(jī)上。哈哈哈,震驚,這瓜味道不錯(cuò)。
最后,由于卷發(fā)哥是個(gè)意大利人,由于最近疫情,四川專家組馳援意大利的事,big thank 中國人。其實(shí)這個(gè)網(wǎng)友的回答挺好的:投桃報(bào)李。

疫情早點(diǎn)過去吧,世界和平。
最后說一句
我寫到這里的時(shí)候,不知不覺已經(jīng)凌晨3點(diǎn)多了,但是因?yàn)橐恢备@兩位大神的激烈討論,我的思維異常的清晰。
寫完之后我也說不出誰對(duì)誰錯(cuò)。我覺得對(duì)于系統(tǒng)的設(shè)計(jì),每個(gè)人的出發(fā)點(diǎn)都不一樣,沒有完美的架構(gòu),沒有普適的架構(gòu),但是在完美和普適能平衡的很好的架構(gòu),就是好的架構(gòu)。