新聞中心
又到了一周一次的分享時(shí)間啦,老規(guī)矩,還是先荒腔走板的聊聊生活。

成都創(chuàng)新互聯(lián)是一家專注于網(wǎng)站建設(shè)、網(wǎng)站制作和綿陽服務(wù)器托管的網(wǎng)絡(luò)公司,有著豐富的建站經(jīng)驗(yàn)和案例。
有上面的圖是讀大學(xué)的時(shí)候,一次自行車騎行途中隊(duì)友抓拍的我的照片。拍照的地方,名字叫做牛背山,一個(gè)名字很 low,實(shí)際很美的地方。
那條上山的路很難騎,超級爛路和極度變態(tài)的陡坡。真是一種折磨,是對意志力的完全考驗(yàn)。
在我們幾近崩潰,彈盡糧絕,離山頂還有近兩個(gè)多小時(shí)的時(shí)候,一個(gè)卡車司機(jī)主動(dòng)要求把我們免費(fèi)帶到山頂。我們拒絕了。
因?yàn)橥械男歉缢f了一句話:“騎行牛背山這件事情,我這輩子只會(huì)做這一次。現(xiàn)在的情況還不是那么糟,如果我搭車上去了,之后想起來我會(huì)有遺憾的。所以我更情愿推車?!?/p>
第二天下午山上下了一場暴風(fēng)雪。于是我們幾個(gè)南方孩子在這少見的雪景中肆意打鬧。打雪仗,堆雪人,滑雪......
晚上雪停了之后,我看到了讓我終身難忘的場景。星空,美麗到讓人想哭的星空!
后來,在人生路上的很多場景中,我都會(huì)想起星哥的那句話:這件事,我這輩子只會(huì)做這一次,我不想留下遺憾。還會(huì)想起那晚璀璨的、觸手可及般的星空。
堅(jiān)持不住的時(shí)候再堅(jiān)持一下,確實(shí)是一種難能可貴的精神。
好了,說回文章。
背景鋪墊
面試的時(shí)候,不管你的簡歷寫沒寫 Redis,它基本上是一個(gè)繞不過的話題。
為了引出本文要討論的關(guān)于 Redlock 的神仙打架的問題,我們就得先通過一個(gè)面試連環(huán)炮:
- Redis 做分布式鎖的時(shí)候有需要注意的問題?
- 如果是 Redis 是單點(diǎn)部署的,會(huì)帶來什么問題?
- 那你準(zhǔn)備怎么解決單點(diǎn)問題呢?
- 集群模式下,比如主從模式,有沒有什么問題呢?
- 你知道 Redis 是怎么解決集群模式也不靠譜的問題的嗎?
- 那你簡單的介紹一下 Redlock 吧?
- 你覺得 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。
基于這場 battle,又引發(fā)了更多的討論。
這場 battle 難分伯仲,沒有最后的贏家。如果一定要選出誰是最大的贏家的話,那一定是吃瓜網(wǎng)友。因?yàn)閷τ诔怨暇W(wǎng)友來說(比如我),可以從兩位大神的交鋒中學(xué)習(xí)到很多東西。
讓你深刻的體會(huì)到:看起來那么無懈可擊的想法,細(xì)細(xì)推敲之下,并不是那么天衣無縫。
所以本文就按照下面的五個(gè)模塊展開講述。
先來一波勸退:本文近1.2w字,謹(jǐn)慎觀看。看不下去不要緊,拉到最后點(diǎn)個(gè)“在看”。就是對于我最大的鼓勵(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)橛械摹澳昃檬蕖钡奈恼轮袑τ?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)在所有客戶端的所有獲取鎖的請求中都是唯一的。
NX:表示只有當(dāng)要設(shè)置的 key 值不存在的時(shí)候才能 set 成功。這保證了只有第一個(gè)請求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖。
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 對應(yīng)的值是否和我指定的值一樣,是一樣的才能釋放鎖。所以可以看到這里有獲取、判斷、刪除三個(gè)操作,為了保障原子性,我們需要用 lua 腳本。
(基本上能答到這幾個(gè)關(guān)鍵點(diǎn),面試官也就會(huì)進(jìn)入下一個(gè)問題了。常規(guī)熱身送分題呀,朋友們,得記住了。)
集群模式
面試官就會(huì)接著問了:
經(jīng)過剛剛的討論,我們已經(jīng)有較好的方法獲取鎖和釋放鎖?;赗edis單實(shí)例,假設(shè)這個(gè)單實(shí)例總是可用,這種方法已經(jīng)足夠安全。如果這個(gè)Redis節(jié)點(diǎn)掛掉了呢?
到這個(gè)問題其實(shí)可以直接聊到 Redlock 了。但是你別慌啊,為了展示你豐富的知識儲備(瘋狂的刷題準(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)境中是很少使用的。
哨兵模式就是主從模式的升級版,該模式下會(huì)對響應(yīng)異常的主節(jié)點(diǎn)進(jìn)行主觀下線或者客觀下線的操作,并進(jìn)行主從切換。它可以保證高可用。
cluster (集群)模式保證的是高并發(fā),整個(gè)集群分擔(dān)所有數(shù)據(jù),不同的 key 會(huì)放到不同的 Redis 中。每個(gè) Redis 對應(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)一把鎖被拿到了兩次的場景。
鎖都被拿了兩次了,也就不滿足安全性了。一個(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 臺服務(wù)器中,可以保證他們不會(huì)同時(shí)宕機(jī)。
從官網(wǎng)上我們可以知道,一個(gè)客戶端如果要獲得鎖,必須經(jīng)過下面的五個(gè)步驟:
步驟描述來源:
http://redis.cn/topics/distlock.html
- 獲取當(dāng)前 Unix 時(shí)間,以毫秒為單位。
- 依次嘗試從 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í)例。
- 客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(步驟 1 記錄的時(shí)間)就得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(這里是 3 個(gè)節(jié)點(diǎn))的 Redis 節(jié)點(diǎn)都取到鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。
- 如果取到了鎖,key 的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟 3 計(jì)算的結(jié)果)。
- 如果因?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è)場景:
我們一共有 A、B、C 這三個(gè)節(jié)點(diǎn)。
- 客戶端 1 在 A,B 上加鎖成功。C 上加鎖失敗。
- 這時(shí)節(jié)點(diǎn) B 崩潰重啟了,但是由于持久化策略導(dǎo)致客戶端 1 在 B 上的鎖沒有持久化下來。
- 客戶端 2 發(fā)起申請同一把鎖的操作,在 B,C 上加鎖成功。
- 這個(gè)時(shí)候就又出現(xiàn)同一把鎖,同時(shí)被客戶端 1 和客戶端 2 所持有了。
(接下來又得說一說Redis的持久化策略了,全是知識點(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)是不對外工作的。那么如果大多數(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)收到加鎖請求了,也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 。有的朋友對英文名字不太敏感,所以后面我就叫他卷發(fā)哥吧。
神仙二:分布式領(lǐng)域?qū)<?Martin Kleppmann,我們叫他長發(fā)哥吧。
看完上面兩位神仙的照片,再看看我為了寫這篇文章又日漸稀少的頭發(fā),我忍不住哭出聲來??赡苤挥薪o我點(diǎn)贊,才能平復(fù)我的心情吧。
卷發(fā)哥在官網(wǎng)介紹 Redlock 頁面的最后寫到:如果你也是使用分布式系統(tǒng)的人員,你的觀點(diǎn)和意見非常重要,歡迎和我們討論。
于是,“求錘得錘”!這一錘,錘出了眾多的吃瓜網(wǎng)友,其中不乏在相關(guān)領(lǐng)域的專業(yè)人士。
長發(fā)哥出錘
故事得從 2016年2月8號 長發(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)在其他地方受到了批評 。
你看這個(gè)文章,開頭就是火藥味十足:你說要反饋,那我就給你反饋。而且你這個(gè)東西有其他問題,我也就不說了。(其實(shí)作者在這篇文章中也說了,他很喜歡并且也在使用 Redis,只是他覺得這個(gè) Redlock 算法是不嚴(yán)謹(jǐn)?shù)?
長發(fā)哥主要圍繞了下面的這張圖進(jìn)行了展開:
要是一眼沒看明白,我再給你一個(gè)中文版的,來自長發(fā)哥于2017年出版的書《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》:
可以看到上面的圖片中提到了申請租約、租約到期的關(guān)鍵詞,租約其實(shí)就是可以理解為帶超時(shí)時(shí)間的鎖。
而在書中,這張圖片的下面寫的描述這樣的,你咂摸咂摸:
拿 HBase 舉例,其設(shè)計(jì)的目標(biāo)是確保存儲系統(tǒng)的文件一次只能由一個(gè)客戶端訪問,如果多個(gè)客戶端試圖同時(shí)寫入該文件,文件就會(huì)被破壞。那么上面的圖片解釋起來就是:
- 客戶端 1 先去申請鎖,并且成功獲取到鎖。之后客戶端進(jìn)行了長時(shí)間的 GC 導(dǎo)致了 STW 的情況。
- 在 STW 期間,客戶端 1 獲取的鎖的超時(shí)時(shí)間到了,鎖也就失效了。
- 由于客戶端 1 的鎖已經(jīng)過期失效了,所以客戶端 2 去申請鎖就可以成功獲得鎖。
- 客戶端 2 開始寫文件,并完成文件的寫入。
- 客戶端 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ù)也申請到了鎖。
對于這種場景,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ì)遞增。
然后,要求客戶端每次向存儲系統(tǒng)發(fā)送寫請求時(shí),都必須包含所持有的 fencing 令牌。存儲系統(tǒng)需要對令牌進(jìn)行校驗(yàn),發(fā)現(xiàn)如果已經(jīng)處理過更高令牌的請求,則拒絕執(zhí)行該請求。
比如下面的圖片:
- 客戶端 1 獲得一個(gè)具有超時(shí)時(shí)間的鎖的同時(shí)得到了令牌號 33,但隨后陷入了一個(gè)長時(shí)間的暫停直到鎖到期。
- 這時(shí)客戶端2已經(jīng)獲得了鎖和令牌號 34 ,然后發(fā)送寫請求(以及令牌號 34 )到存儲服務(wù)。
- 接下來客戶端 1 恢復(fù)過來,并以令牌號 33 來嘗試寫入,存儲服務(wù)器由于記錄了最近已經(jīng)完成了更高令牌號(34 ),因此拒絕令牌號 33 的寫請求。
這種版本號的機(jī)制,讓我不禁想起了 Zookeeper。當(dāng)使用 ZK 做鎖服務(wù)時(shí),可以用事務(wù)標(biāo)識 zxid 或節(jié)點(diǎn)版本 cversion 來充當(dāng) fencing 令牌,這兩個(gè)都可以滿足單調(diào)遞增的要求。
在長發(fā)哥的這種機(jī)制中,實(shí)際上就是要求資源本身必須主動(dòng)檢查請求所持令牌信息,如果發(fā)現(xiàn)已經(jīng)處理過更高令牌的請求,要拒絕持有低令牌的所有寫請求。
但是,不是所有的資源都是數(shù)據(jù)庫里面的數(shù)據(jù),我們可以通過版本號去支持額外的令牌檢查的,那么對于不支持額外的令牌檢查資源,我們也可以借助這種思想繞過這個(gè)限制,比如對于訪問文件存儲服務(wù)的情況,我們可以將令牌嵌入到文件名中。
總之,為了避免在鎖保護(hù)之外發(fā)生請求處理,需要進(jìn)行額外的檢查機(jī)制。
長發(fā)哥在書中也說到了:在服務(wù)端檢查令牌可能看起來有點(diǎn)復(fù)雜,但是這其實(shí)是推薦的正確的做法:系統(tǒng)服務(wù)不能假定所有的客戶端都表現(xiàn)的符合預(yù)期。從安全角度講,服務(wù)端必須防范這種來自客戶端的濫用。
這個(gè)就類似于我們作為后端開發(fā)人員,也不能相信來自前端或者其他接口過來的數(shù)據(jù),必須對其進(jìn)行校驗(yàn)。
到這里長發(fā)哥鋪墊完成了,開始轉(zhuǎn)頭指向 RedLock,他認(rèn)為 Redlock 是一個(gè)嚴(yán)重依賴系統(tǒng)時(shí)鐘的分布式鎖。
他舉了一個(gè)例子:
- 客戶端 1 從 Redis 節(jié)點(diǎn) A, B, C 成功獲取了鎖。由于網(wǎng)絡(luò)問題,無法訪問 D 和 E。
- 節(jié)點(diǎn) C 上的時(shí)鐘發(fā)生了向前跳躍,導(dǎo)致它上面維護(hù)的鎖過期了。
- 客戶端 2 從 Redis 節(jié)點(diǎn) C, D, E 成功獲取了同一個(gè)資源的鎖。由于網(wǎng)絡(luò)問題,無法訪問 A 和 B。
- 現(xiàn)在,客戶端 1 和客戶端 2 都認(rèn)為自己持有了鎖。
這樣的場景是可能出現(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 的安全級別是不夠的。
而對于卷發(fā)哥提出的延遲啟動(dòng)方案,長發(fā)哥還是一棒子打死:你延遲啟動(dòng)咋的?延遲啟動(dòng)還不是依賴于合理準(zhǔn)確的時(shí)間度量。
可能是長發(fā)哥覺得舉這個(gè)時(shí)鐘跳躍的例子不夠好的,大家都可能認(rèn)為時(shí)鐘跳躍是不現(xiàn)實(shí)的,因?yàn)閷φ_配置NTP就能擺正時(shí)鐘非常有信心。
在這種情況下,他舉了一個(gè)進(jìn)程暫??赡軐?dǎo)致算法失敗的示例:
- 客戶端 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 階段。
- 長時(shí)間的 GC,導(dǎo)致在所有的 Redis 節(jié)點(diǎn)上,鎖過期了。
- 客戶端 2 在 A, B, C, D, E 上申請并獲取到了鎖。
- 客戶端 1 從 GC 階段中恢復(fù),收到了前面第 2 步來自各個(gè) Redis 節(jié)點(diǎn)的請求結(jié)果。客戶端 1 認(rèn)為自己成功獲取到了鎖。
- 客戶端 1 和客戶端 2 現(xiàn)在都認(rèn)為自己持有了鎖。
其實(shí)只要十分清楚 Redlock 的加鎖過程,我們就知道,這種情況其實(shí)對于 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ù)的工作。這種場景下,即使鎖偶爾失效了,只是可能出現(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(不倫不類)
對于提升效率的場景下,使用分布式鎖,允許鎖的偶爾失效,那么使用單 Redis 節(jié)點(diǎn)的鎖方案就足夠了,簡單而且效率高。用 Redlock 太重。
對于正確性要求高的場景下,它是依賴于時(shí)間的,不是一個(gè)足夠強(qiáng)的算法。Redlock并沒有保住正確性。
那應(yīng)該使用什么技術(shù)呢?
長發(fā)哥認(rèn)為,應(yīng)該考慮類似 Zookeeper 的方案,或者支持事務(wù)的數(shù)據(jù)庫。
卷發(fā)哥回?fù)?/strong>
長發(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è)方面:
- 帶有自動(dòng)過期功能的分布式鎖,需要一種方法(fencing機(jī)制)來避免客戶端在過期時(shí)間后使用鎖時(shí)出現(xiàn)問題,從而對共享資源進(jìn)行真正的互斥保護(hù)。馬丁說Redlock沒有這種機(jī)制。
- 馬丁說,無論問題“1”如何解決,該算法本質(zhì)上都是不安全的,因?yàn)樗鼘ο到y(tǒng)模型進(jìn)行了記時(shí)假設(shè),而這些假設(shè)在實(shí)際系統(tǒng)中是無法保證的。
對于第一個(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è)問題,對于網(wǎng)絡(luò)延遲或者 GC 暫停,我們前面分析過,對 Redlock 的安全性并不會(huì)產(chǎn)生影響,說明卷發(fā)哥在設(shè)計(jì)的時(shí)候其實(shí)是考慮過時(shí)間因素帶來的問題的。
但是如果是長發(fā)哥提出的時(shí)鐘發(fā)生跳躍,很明顯,卷發(fā)哥知道如果時(shí)鐘發(fā)生跳躍, Redlock 的安全性就得不到保障,這是他的命門。
但是對于長發(fā)哥寫時(shí)鐘跳躍的時(shí)候提出的兩個(gè)例子:
- 運(yùn)維人員手動(dòng)修改了系統(tǒng)時(shí)鐘。
- 從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í)鐘更新,對于這個(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)就很明確了,就是大延遲對分布式鎖帶來的影響。
而對于大延遲給Redlock帶來的影響,就是長發(fā)哥分析的那樣,鎖到期了,業(yè)務(wù)還沒執(zhí)行完。卷發(fā)哥認(rèn)為這種影響不單單針對 Redlock ,其他具有自動(dòng)釋放鎖的分布式鎖也是存在一樣的問題。
而關(guān)于大延遲的問題,我在某社交平臺上找到了兩位神仙的下面的對話:
卷發(fā)哥問:我想知道,在我發(fā)文回復(fù)之后,我們能否在一點(diǎn)上達(dá)成一致,就是大的消息延遲不會(huì)給Redlock的運(yùn)行造成損害。
長發(fā)哥答:對于客戶端和鎖服務(wù)器之間的消息延遲,我同意你的觀點(diǎn)。但客戶端和被訪問資源之間的延遲還是有問題的。
所以通過卷發(fā)哥的回?fù)粑恼潞湍成缃黄脚_的記錄,他是同意大的系統(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)的使用者,讀完他們的文章以及由此文章衍生出來的知識點(diǎn)后,受益良多,于是寫下此文作為學(xué)習(xí)總結(jié),也與大家分享。本文還有很多不足之處,還請各位海涵。
如同文章開篇說的,這場爭論沒有最后的贏家。很明顯卷發(fā)哥是沒有說服長發(fā)哥的,因?yàn)樵陂L發(fā)哥2017年出版的《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》一書中,專門有一小節(jié)的名稱叫做:不可靠的時(shí)鐘
其實(shí)在這場爭論的最后,長發(fā)哥對這場爭論進(jìn)行了一個(gè)非常感性的總結(jié),他說:
下面翻譯來自:
https://www.jianshu.com/p/dd66bdd18a56
對我來說最重要的一點(diǎn)在于:我并不在乎在這場辯論中誰對誰錯(cuò) —— 我只關(guān)心從其他人的工作中學(xué)到的東西,以便我們能夠避免重蹈覆轍,并讓未來更加美好。前人已經(jīng)為我們創(chuàng)造出了許多偉大的成果:站在巨人的肩膀上,我們得以構(gòu)建更棒的軟件。
對于任何想法,務(wù)必要詳加檢驗(yàn),通過論證以及檢查它們是否經(jīng)得住別人的詳細(xì)審查。那是學(xué)習(xí)過程的一部分。但目標(biāo)應(yīng)該是為了獲得知識,而不應(yīng)該是為了說服別人相信你自己是對的。有時(shí)候,那只不過意味著停下來,好好地想一想。
吃瓜網(wǎng)友的收獲
這里的吃瓜網(wǎng)友就是指我啦。
寫這篇文章我的收獲還是挺大的,首先我買了長發(fā)哥的《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》一書,讀了幾節(jié),發(fā)現(xiàn)這書是真的不錯(cuò),豆瓣評分9.6,當(dāng)當(dāng)最近也在搞活動(dòng),52元就能拿下,強(qiáng)推:
廣告數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)
作者:[美] Martin Kleppmann(馬丁·科勒普曼) 著 譯者:趙軍平 呂云松 耿煜 李三平
其次完成了這周的周更任務(wù),雖然寫的很艱難,從周六中午,寫到周日凌晨3點(diǎn)。。。
然后還吃到了另外的一個(gè)瓜,可謂是瓜中瓜。
這周五的時(shí)候 Redis 官網(wǎng)不是出現(xiàn)了短暫的宕機(jī)嗎,宕機(jī)其實(shí)也沒啥稀奇的,但是頁面上顯示的是連不上 Redis 。這就有點(diǎn)意思了。
我在寫這篇文章的時(shí)候,在卷發(fā)哥的某社交平臺上發(fā)現(xiàn)了這個(gè):
我關(guān)心的并不是 OOM,而是卷發(fā)哥居然讓 Redis 官網(wǎng)運(yùn)行在一臺一個(gè)月僅 5 美元,內(nèi)存只有 1G 的虛擬機(jī)上。哈哈哈,震驚,這瓜味道不錯(cuò)。
最后,由于卷發(fā)哥是個(gè)意大利人,由于最近疫情,四川專家組馳援意大利的事,big thank 中國人。其實(shí)這個(gè)網(wǎng)友的回答挺好的:投桃報(bào)李。
疫情早點(diǎn)過去吧,世界和平。
最后說一句(求關(guān)注)
我寫到這里的時(shí)候,不知不覺已經(jīng)凌晨3點(diǎn)多了,但是因?yàn)橐恢备@兩位大神的激烈討論,我的思維異常的清晰。
寫完之后我也說不出誰對誰錯(cuò)。我覺得對于系統(tǒng)的設(shè)計(jì),每個(gè)人的出發(fā)點(diǎn)都不一樣,沒有完美的架構(gòu),沒有普適的架構(gòu),但是在完美和普適能平衡的很好的架構(gòu),就是好的架構(gòu)。
瞟了一眼文章字?jǐn)?shù),快突破了1.2w字。可能又是一篇寫了沒人看的勸退文吧,但是沒有關(guān)系,只要有一個(gè)人看了我的文章覺得有幫助就行。
點(diǎn)個(gè)“在看”吧,寫文章很累的,不要白嫖我,需要一點(diǎn)正反饋。
才疏學(xué)淺,難免會(huì)有紕漏,如果你發(fā)現(xiàn)了錯(cuò)誤的地方,還請你留言給我指出來,我對其加以修改。(我每篇技術(shù)文章都有這句話,我是認(rèn)真的說的。)
文章名稱:【求錘得錘的故事】Redis鎖從面試連環(huán)炮聊到神仙打架
網(wǎng)頁地址:http://www.5511xx.com/article/dpcpppi.html


咨詢
建站咨詢
