【求錘得錘的故事】Redis鎖從面試連環(huán)炮聊到神仙打架
又到了一周一次的分享時間啦,老規(guī)矩,還是先荒腔走板的聊聊生活。
有上面的圖是讀大學的時候,一次自行車騎行途中隊友抓拍的我的照片。拍照的地方,名字叫做牛背山,一個名字很 low,實際很美的地方。
那條上山的路很難騎,超級爛路和極度變態(tài)的陡坡。真是一種折磨,是對意志力的完全考驗。
在我們幾近崩潰,彈盡糧絕,離山頂還有近兩個多小時的時候,一個卡車司機主動要求把我們免費帶到山頂。我們拒絕了。
因為同行的星哥他說了一句話:“騎行牛背山這件事情,我這輩子只會做這一次?,F(xiàn)在的情況還不是那么糟,如果我搭車上去了,之后想起來我會有遺憾的。所以我更情愿推車。”
第二天下午山上下了一場暴風雪。于是我們幾個南方孩子在這少見的雪景中肆意打鬧。打雪仗,堆雪人,滑雪......
晚上雪停了之后,我看到了讓我終身難忘的場景。星空,美麗到讓人想哭的星空!
后來,在人生路上的很多場景中,我都會想起星哥的那句話:這件事,我這輩子只會做這一次,我不想留下遺憾。還會想起那晚璀璨的、觸手可及般的星空。
堅持不住的時候再堅持一下,確實是一種難能可貴的精神。
好了,說回文章。
背景鋪墊
面試的時候,不管你的簡歷寫沒寫 Redis,它基本上是一個繞不過的話題。
為了引出本文要討論的關于 Redlock 的神仙打架的問題,我們就得先通過一個面試連環(huán)炮:
- Redis 做分布式鎖的時候有需要注意的問題?
- 如果是 Redis 是單點部署的,會帶來什么問題?
- 那你準備怎么解決單點問題呢?
- 集群模式下,比如主從模式,有沒有什么問題呢?
- 你知道 Redis 是怎么解決集群模式也不靠譜的問題的嗎?
- 那你簡單的介紹一下 Redlock 吧?
- 你覺得 Redlock 有什么問題呢?
很明顯,上面是一個常規(guī)的面試連環(huán)套路題。中間還可以插入很多其他的 Redis 的考察點,我這里就不做擴展了。
單點的 Redis 做分布式鎖不靠譜,導致了基于 Redis 集群模式的分布式鎖解決方案的出現(xiàn)。
基于 Redis 集群模式的分布式鎖解決方案還是不靠譜,Redis 的作者提出了 Redlock 的解決方案。
Redis 作者提出的 Redlock 的解決方案,另一位分布式系統(tǒng)的大神覺得它不靠譜,于是他們之間開始了 battle。
基于這場 battle,又引發(fā)了更多的討論。
這場 battle 難分伯仲,沒有最后的贏家。如果一定要選出誰是最大的贏家的話,那一定是吃瓜網友。因為對于吃瓜網友來說(比如我),可以從兩位大神的交鋒中學習到很多東西。
讓你深刻的體會到:看起來那么無懈可擊的想法,細細推敲之下,并不是那么天衣無縫。
所以本文就按照下面的五個模塊展開講述。
先來一波勸退:本文近1.2w字,謹慎觀看。看不下去不要緊,拉到最后點個“在看”。就是對于我最大的鼓勵。奧利給!
單點Redis
按照我的經驗,當面試聊到 Redis 的時候,百分之 90 的朋友都會說:Redis在我們的項目中是用來做熱點數(shù)據(jù)緩存的。
然后百分之百的面試官都會問:
Redis除了拿來做緩存,你還見過基于Redis的什么用法?
接下來百分之 80 的朋友都會說到:我們還用 Redis 做過分布式鎖。
(當然, Redis 除了緩存、分布式鎖之外還有非常非常多的奇技淫巧,不是本文重點,大家有興趣的可以自己去了解一下。)
那么面試官就會接著說:
那你給我描述(或者寫一下偽代碼)基于Redis的加鎖和釋放鎖的細節(jié)吧。
注意面試官這里說的是加鎖和釋放鎖的細節(jié),魔鬼都在細節(jié)里。
問這個問題面試官無非是想要聽到下面幾個關鍵點:
關鍵點一:原子命令加鎖。因為有的“年久失修”的文章中對于 Redis 的加鎖操作是先set key,再設置 key 的過期時間。這樣寫的根本原因是在早期的 Redis 版本中并不支持原子命令加鎖的操作。不是原子操作會帶來什么問題,就不用我說了吧?如果你不知道,你先回去等通知吧。
而在 2.6.12 版本后,可以通過向 Redis 發(fā)送下面的命令,實現(xiàn)原子性的加鎖操作:
SET key random_value NX PX 30000
關鍵點二:設置值的時候,放的是random_value。而不是你隨便扔個“OK”進去。
先解釋一下上面的命令中的幾個參數(shù)的含義:
random_value:是由客戶端生成的一個隨機字符串,它要保證在足夠長的一段時間內在所有客戶端的所有獲取鎖的請求中都是唯一的。
NX:表示只有當要設置的 key 值不存在的時候才能 set 成功。這保證了只有第一個請求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖。
PX 30000:表示這個鎖有一個 30 秒的自動過期時間。當然,這里 30 秒只是一個例子,客戶端可以選擇合適的過期時間。
再解釋一下為什么 value 需要設置為一個隨機字符串。這也是第三個關鍵點。
關鍵點三:value 的值設置為隨機數(shù)主要是為了更安全的釋放鎖,釋放鎖的時候需要檢查 key 是否存在,且 key 對應的值是否和我指定的值一樣,是一樣的才能釋放鎖。所以可以看到這里有獲取、判斷、刪除三個操作,為了保障原子性,我們需要用 lua 腳本。
(基本上能答到這幾個關鍵點,面試官也就會進入下一個問題了。常規(guī)熱身送分題呀,朋友們,得記住了。)
集群模式
面試官就會接著問了:
經過剛剛的討論,我們已經有較好的方法獲取鎖和釋放鎖?;赗edis單實例,假設這個單實例總是可用,這種方法已經足夠安全。如果這個Redis節(jié)點掛掉了呢?
到這個問題其實可以直接聊到 Redlock 了。但是你別慌啊,為了展示你豐富的知識儲備(瘋狂的刷題準備),你得先自己聊一聊 Redis 的集群,你可以這樣去說:
為了避免節(jié)點掛掉導致的問題,我們可以采用Redis集群的方法來實現(xiàn)Redis的高可用。
Redis集群方式共有三種:主從模式,哨兵模式,cluster(集群)模式
其中主從模式會保證數(shù)據(jù)在從節(jié)點還有一份,但是主節(jié)點掛了之后,需要手動把從節(jié)點切換為主節(jié)點。它非常簡單,但是在實際的生產環(huán)境中是很少使用的。
哨兵模式就是主從模式的升級版,該模式下會對響應異常的主節(jié)點進行主觀下線或者客觀下線的操作,并進行主從切換。它可以保證高可用。
cluster (集群)模式保證的是高并發(fā),整個集群分擔所有數(shù)據(jù),不同的 key 會放到不同的 Redis 中。每個 Redis 對應一部分的槽。
(上面三種模式也是面試重點,可以說很多道道出來,由于不是本文重點就不詳細描述了。主要表達的意思是你得在面試的時候遇到相關問題,需要展示自己是知道這些東西的,都是面試的套路。)
在上面描述的集群模式下還是會出現(xiàn)一個問題,由于節(jié)點之間是采用異步通信的方式。如果剛剛在 Master 節(jié)點上加了鎖,但是數(shù)據(jù)還沒被同步到 Salve。這時 Master 節(jié)點掛了,它上面的鎖就沒了,等新的 Master 出來后(主從模式的手動切換或者哨兵模式的一次 failover 的過程),就可以再次獲取同樣的鎖,出現(xiàn)一把鎖被拿到了兩次的場景。
鎖都被拿了兩次了,也就不滿足安全性了。一個安全的鎖,不管是不是分布式的,在任意一個時刻,都只有一個客戶端持有。
Redlock簡介
為了解決上面的問題,Redis 的作者提出了名為 Redlock 的算法。
在 Redis 的分布式環(huán)境中,我們假設有 N 個 Redis Master。這些節(jié)點完全互相獨立,不存在主從復制或者其他集群協(xié)調機制。
前面已經描述了在單點 Redis 下,怎么安全地獲取和釋放鎖,我們確保將在 N 個實例上使用此方法獲取和釋放鎖。
在下面的示例中,我們假設有 5 個完全獨立的 Redis Master 節(jié)點,他們分別運行在 5 臺服務器中,可以保證他們不會同時宕機。
從官網上我們可以知道,一個客戶端如果要獲得鎖,必須經過下面的五個步驟:
步驟描述來源:
http://redis.cn/topics/distlock.html
- 獲取當前 Unix 時間,以毫秒為單位。
- 依次嘗試從 N 個實例,使用相同的 key 和隨機值獲取鎖。在步驟 2,當向 Redis 設置鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小于鎖的失效時間。例如你的鎖自動失效時間為 10 秒,則超時時間應該在 5-50 毫秒之間。這樣可以避免服務器端 Redis 已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規(guī)定時間內響應,客戶端應該盡快嘗試另外一個 Redis 實例。
- 客戶端使用當前時間減去開始獲取鎖時間(步驟 1 記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(shù)(這里是 3 個節(jié)點)的 Redis 節(jié)點都取到鎖,并且使用的時間小于鎖失效時間時,鎖才算獲取成功。
- 如果取到了鎖,key 的真正有效時間等于有效時間減去獲取鎖所使用的時間(步驟 3 計算的結果)。
- 如果因為某些原因,獲取鎖失敗(沒有在至少 N/2+1 個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的 Redis 實例上進行解鎖(即便某些 Redis 實例根本就沒有加鎖成功)。
通過上面的步驟我們可以知道,只要大多數(shù)的節(jié)點可以正常工作,就可以保證 Redlock 的正常工作。這樣就可以解決前面單點 Redis 的情況下我們討論的節(jié)點掛掉,由于異步通信,導致鎖失效的問題。
但是,還是不能解決故障重啟后帶來的鎖的安全性的問題。你想一下下面這個場景:
我們一共有 A、B、C 這三個節(jié)點。
- 客戶端 1 在 A,B 上加鎖成功。C 上加鎖失敗。
- 這時節(jié)點 B 崩潰重啟了,但是由于持久化策略導致客戶端 1 在 B 上的鎖沒有持久化下來。
- 客戶端 2 發(fā)起申請同一把鎖的操作,在 B,C 上加鎖成功。
- 這個時候就又出現(xiàn)同一把鎖,同時被客戶端 1 和客戶端 2 所持有了。
(接下來又得說一說Redis的持久化策略了,全是知識點啊,朋友們)
比如,Redis 的 AOF 持久化方式默認情況下是每秒寫一次磁盤,即 fsync 操作,因此最壞的情況下可能丟失 1 秒的數(shù)據(jù)。
當然,你也可以設置成每次修改數(shù)據(jù)都進行 fsync 操作(fsync=always),但這會嚴重降低 Redis 的性能,違反了它的設計理念。(我也沒見過這樣用的,可能還是見的太少了吧。)
而且,你以為執(zhí)行了 fsync 就不會丟失數(shù)據(jù)了?天真,真實的系統(tǒng)環(huán)境是復雜的,這都已經脫離 Redis 的范疇了。上升到服務器、系統(tǒng)問題了。
所以,根據(jù)墨菲定律,上面舉的例子:由于節(jié)點重啟引發(fā)的鎖失效問題,總是有可能出現(xiàn)的。
為了解決這一問題,Redis 的作者又提出了延遲重啟(delayed restarts)的概念。
意思就是說,一個節(jié)點崩潰后,不要立即重啟它,而是等待一定的時間后再重啟。等待的時間應該大于鎖的過期時間(TTL)。這樣做的目的是保證這個節(jié)點在重啟前所參與的鎖都過期。相當于把以前的帳勾銷之后才能參與后面的加鎖操作。
但是有個問題就是:在等待的時間內,這個節(jié)點是不對外工作的。那么如果大多數(shù)節(jié)點都掛了,進入了等待。就會導致系統(tǒng)的不可用,因為系統(tǒng)在TTL時間內任何鎖都將無法加鎖成功。
Redlock 算法還有一個需要注意的點是它的釋放鎖操作。
釋放鎖的時候是要向所有節(jié)點發(fā)起釋放鎖的操作的。這樣做的目的是為了解決有可能在加鎖階段,這個節(jié)點收到加鎖請求了,也set成功了,但是由于返回給客戶端的響應包丟了,導致客戶端以為沒有加鎖成功。所有,釋放鎖的時候要向所有節(jié)點發(fā)起釋放鎖的操作。
你可以覺得這不是常規(guī)操作嗎?
有的細節(jié)就是這樣,說出來后覺得不過如此,但是有可能自己就是想不到這個點,導致問題的出現(xiàn),所以我們才會說:細節(jié),魔鬼都在細節(jié)里。
好了,簡介大概就說到這里,有興趣的朋友可以再去看看官網,補充一下。
中文:http://redis.cn/topics/distlock.html
英文:https://redis.io/topics/distlock
好了,經過這么長,這么長的鋪墊,我們終于可以進入到神仙打架環(huán)節(jié)。
神仙打架
神仙一:Redis 的作者 antirez 。有的朋友對英文名字不太敏感,所以后面我就叫他卷發(fā)哥吧。
神仙二:分布式領域專家 Martin Kleppmann,我們叫他長發(fā)哥吧。
看完上面兩位神仙的照片,再看看我為了寫這篇文章又日漸稀少的頭發(fā),我忍不住哭出聲來??赡苤挥薪o我點贊,才能平復我的心情吧。
卷發(fā)哥在官網介紹 Redlock 頁面的最后寫到:如果你也是使用分布式系統(tǒ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ù)密集型應用系統(tǒng)設計》)研究的一部分,我在Redis網站上 看到了一種稱為Redlock的算法。該算法聲稱在Redis實現(xiàn)容錯的分布式鎖(或更確切地說, 租約),并且該頁面要求來自分布式系統(tǒng)人員的反饋。這個算法讓我產生了一些思考,因此我花了一些時間寫了我的這篇文章。
由于Redlock已經有10多個獨立的實現(xiàn),而且我們不知道誰已經在依賴此算法,因此我認為值得公開分享我的筆記。我不會討論Redis的其他方面,其中一些已經在其他地方受到了批評 。
你看這個文章,開頭就是火藥味十足:你說要反饋,那我就給你反饋。而且你這個東西有其他問題,我也就不說了。(其實作者在這篇文章中也說了,他很喜歡并且也在使用 Redis,只是他覺得這個 Redlock 算法是不嚴謹?shù)?
長發(fā)哥主要圍繞了下面的這張圖進行了展開:
要是一眼沒看明白,我再給你一個中文版的,來自長發(fā)哥于2017年出版的書《數(shù)據(jù)密集型應用系統(tǒng)設計》:
可以看到上面的圖片中提到了申請租約、租約到期的關鍵詞,租約其實就是可以理解為帶超時時間的鎖。
而在書中,這張圖片的下面寫的描述這樣的,你咂摸咂摸:
拿 HBase 舉例,其設計的目標是確保存儲系統(tǒng)的文件一次只能由一個客戶端訪問,如果多個客戶端試圖同時寫入該文件,文件就會被破壞。那么上面的圖片解釋起來就是:
- 客戶端 1 先去申請鎖,并且成功獲取到鎖。之后客戶端進行了長時間的 GC 導致了 STW 的情況。
- 在 STW 期間,客戶端 1 獲取的鎖的超時時間到了,鎖也就失效了。
- 由于客戶端 1 的鎖已經過期失效了,所以客戶端 2 去申請鎖就可以成功獲得鎖。
- 客戶端 2 開始寫文件,并完成文件的寫入。
- 客戶端 1 從 STW 中恢復過來,他并不知道自己的鎖過期了,還是會繼續(xù)執(zhí)行文件寫入操作,導致客戶端 2 寫入的文件被破壞。而且可以看到,它沒有滿足鎖在任意時刻只有一個客戶端持有的原則,即沒有滿足互斥性。
書里面沒有明說,但是你品一品,這里的鎖服務難道不是在說 Redis?
有的朋友就會說了,那客戶端 1 寫入文件的時候,再判斷一下自己的鎖有沒有過期不就可以了嗎?
你可真是個小機靈鬼呢,那我問你,GC 可能是發(fā)生在任何時間的,萬一 GC 發(fā)生在判斷之后呢?
你繼續(xù)懟我,如果客戶端使用的是沒有 GC 的語言呢?
GC 不是導致線程暫停的唯一原因啊,朋友們。發(fā)生這種情況的原因有很多的,你看看長發(fā)哥書里舉的例子:
上面的內容總結起來,就是就算鎖服務是正常的,但是由于鎖是有持有時間的,由于客戶端阻塞、長時間的 GC 或者網絡原因,導致共享資源被一個以上的客戶端同時訪問了。
其實上面長發(fā)哥在書里直接說了:這是不正確的實現(xiàn)。
你多品一品,上面的圖是不是有點像由于 Redis 鎖的過期時間設置的不合理,導致前一個任務還沒執(zhí)行完成,但是鎖的時間到期了,后一個任務也申請到了鎖。
對于這種場景,Redission 其實有自己的看門狗機制。但是不在這次 Redlock 的討論范圍內,所以這里就不描述了。
長發(fā)哥提出的解決方案是什么呢?
他稱為:fencing token。
長發(fā)哥認為使用鎖和租約機制來保護資源的并發(fā)訪問時,必須確保因為異常原因,導致鎖過期的那個節(jié)點不能影響其他正常的部分,要實現(xiàn)這一目標,可以采用一直相當簡單的 fencing(柵欄)。
假設每次鎖服務在授予鎖或者租約時,還會同時返回一個 fencing 令牌,該令牌每次授予都會遞增。
然后,要求客戶端每次向存儲系統(tǒng)發(fā)送寫請求時,都必須包含所持有的 fencing 令牌。存儲系統(tǒng)需要對令牌進行校驗,發(fā)現(xiàn)如果已經處理過更高令牌的請求,則拒絕執(zhí)行該請求。
比如下面的圖片:
- 客戶端 1 獲得一個具有超時時間的鎖的同時得到了令牌號 33,但隨后陷入了一個長時間的暫停直到鎖到期。
- 這時客戶端2已經獲得了鎖和令牌號 34 ,然后發(fā)送寫請求(以及令牌號 34 )到存儲服務。
- 接下來客戶端 1 恢復過來,并以令牌號 33 來嘗試寫入,存儲服務器由于記錄了最近已經完成了更高令牌號(34 ),因此拒絕令牌號 33 的寫請求。
這種版本號的機制,讓我不禁想起了 Zookeeper。當使用 ZK 做鎖服務時,可以用事務標識 zxid 或節(jié)點版本 cversion 來充當 fencing 令牌,這兩個都可以滿足單調遞增的要求。
在長發(fā)哥的這種機制中,實際上就是要求資源本身必須主動檢查請求所持令牌信息,如果發(fā)現(xiàn)已經處理過更高令牌的請求,要拒絕持有低令牌的所有寫請求。
但是,不是所有的資源都是數(shù)據(jù)庫里面的數(shù)據(jù),我們可以通過版本號去支持額外的令牌檢查的,那么對于不支持額外的令牌檢查資源,我們也可以借助這種思想繞過這個限制,比如對于訪問文件存儲服務的情況,我們可以將令牌嵌入到文件名中。
總之,為了避免在鎖保護之外發(fā)生請求處理,需要進行額外的檢查機制。
長發(fā)哥在書中也說到了:在服務端檢查令牌可能看起來有點復雜,但是這其實是推薦的正確的做法:系統(tǒng)服務不能假定所有的客戶端都表現(xiàn)的符合預期。從安全角度講,服務端必須防范這種來自客戶端的濫用。
這個就類似于我們作為后端開發(fā)人員,也不能相信來自前端或者其他接口過來的數(shù)據(jù),必須對其進行校驗。
到這里長發(fā)哥鋪墊完成了,開始轉頭指向 RedLock,他認為 Redlock 是一個嚴重依賴系統(tǒng)時鐘的分布式鎖。
他舉了一個例子:
- 客戶端 1 從 Redis 節(jié)點 A, B, C 成功獲取了鎖。由于網絡問題,無法訪問 D 和 E。
- 節(jié)點 C 上的時鐘發(fā)生了向前跳躍,導致它上面維護的鎖過期了。
- 客戶端 2 從 Redis 節(jié)點 C, D, E 成功獲取了同一個資源的鎖。由于網絡問題,無法訪問 A 和 B。
- 現(xiàn)在,客戶端 1 和客戶端 2 都認為自己持有了鎖。
這樣的場景是可能出現(xiàn)的,因為 Redlock 嚴重依賴系統(tǒng)時鐘,所以一旦系統(tǒng)的時間變得不準確了,那么該算法的安全性也就得不到保障了。
長發(fā)哥舉這個例子其實是為了輔佐他前面提出的觀點:一個好的分布式算法應該是基于異步模型的,算法的安全性不應該依賴與任何記時假設,就是不能把時間作為安全保障的。在異步模型中,程序暫停、消息在網絡中延遲甚至丟失、系統(tǒng)時間錯誤這些因素都不應該影響它的安全性,只能影響到它的活性。
用大白話說,就是在極其極端的情況下,分布式系統(tǒng)頂天了也就是在有限的時間內不能給出結果而已,而不能給出一個錯誤的結果。
這樣的算法實際上是存在的,比如 Paxos、Raft。很明顯,按照這個標準, Redlock 的安全級別是不夠的。
而對于卷發(fā)哥提出的延遲啟動方案,長發(fā)哥還是一棒子打死:你延遲啟動咋的?延遲啟動還不是依賴于合理準確的時間度量。
可能是長發(fā)哥覺得舉這個時鐘跳躍的例子不夠好的,大家都可能認為時鐘跳躍是不現(xiàn)實的,因為對正確配置NTP就能擺正時鐘非常有信心。
在這種情況下,他舉了一個進程暫??赡軐е滤惴ㄊ〉氖纠?/p>
- 客戶端 1 向 Redis 節(jié)點 A, B, C, D, E 發(fā)起鎖請求。
- 各個 Redis 節(jié)點已經把請求結果返回給了客戶端 1,但客戶端 1 在收到請求結果之前進入了長時間的 GC 階段。
- 長時間的 GC,導致在所有的 Redis 節(jié)點上,鎖過期了。
- 客戶端 2 在 A, B, C, D, E 上申請并獲取到了鎖。
- 客戶端 1 從 GC 階段中恢復,收到了前面第 2 步來自各個 Redis 節(jié)點的請求結果。客戶端 1 認為自己成功獲取到了鎖。
- 客戶端 1 和客戶端 2 現(xiàn)在都認為自己持有了鎖。
其實只要十分清楚 Redlock 的加鎖過程,我們就知道,這種情況其實對于 Redlock 是沒有影響的,因為在第 5 步,客戶端 1 從 GC 階段中恢復過來以后,在 Redlock 算法中,(我們前面 Redlock 簡介的時候提到的第四步)如果取到了鎖,key 的真正有效時間等于有效時間減去獲取鎖所使用的時間。
所以客戶端1通過這個檢查發(fā)現(xiàn)鎖已經過期了,不會再認為自己成功獲取到鎖了。
而隨后卷發(fā)哥的回擊中也提到了這點。
但是,細細想來,我覺得長發(fā)哥的意圖不在于此。拋開上面的問題來講,他更想突出的是,一個鎖在客戶端拿到后,還沒使用就過期了,這是不好的。從客戶端的角度來看,就是這玩意不靠譜啊,你給我一把鎖,我還沒用呢,你就過期了?
除了上面說的這些點外,長發(fā)哥還提出了一個算是自己的經驗之談吧:
我們獲取鎖的用途是什么?
在他看來不外乎兩個方面,效率和正確性。他分別描述如下:
如果是為了效率,那么就是要協(xié)調各個客戶端,避免他們做重復的工作。這種場景下,即使鎖偶爾失效了,只是可能出現(xiàn)兩個客戶端完成了同樣的工作,其結果是成本略有增加(您最終向 AWS 支付的費用比原本多5美分),或者帶來不便(例如,用戶最終兩次收到相同的電子郵件通知)。
如果是為了正確性,那么在任何情況下都不允許鎖失效的情況發(fā)生,因為一旦發(fā)生,就可能意味著數(shù)據(jù)不一致,數(shù)據(jù)丟失,文件損壞,或者其它嚴重的問題。(比如個患者注射了兩倍的藥劑)
最后,長發(fā)哥得出的結論是:neither fish nor fowl(不倫不類)
對于提升效率的場景下,使用分布式鎖,允許鎖的偶爾失效,那么使用單 Redis 節(jié)點的鎖方案就足夠了,簡單而且效率高。用 Redlock 太重。
對于正確性要求高的場景下,它是依賴于時間的,不是一個足夠強的算法。Redlock并沒有保住正確性。
那應該使用什么技術呢?
長發(fā)哥認為,應該考慮類似 Zookeeper 的方案,或者支持事務的數(shù)據(jù)庫。
卷發(fā)哥回擊
長發(fā)哥發(fā)出《How to do distributed locking》這篇文章的第二天,卷發(fā)哥就進行了回擊,發(fā)布了名為《Is Redlock safe?》的文章。
文章地址:http://antirez.com/news/101
要說大佬不愧是大佬,卷發(fā)哥的回擊條理清楚,行文流暢。他總結后認為長發(fā)哥覺得 Redlock 不安全主要分為兩個方面:
- 帶有自動過期功能的分布式鎖,需要一種方法(fencing機制)來避免客戶端在過期時間后使用鎖時出現(xiàn)問題,從而對共享資源進行真正的互斥保護。馬丁說Redlock沒有這種機制。
- 馬丁說,無論問題“1”如何解決,該算法本質上都是不安全的,因為它對系統(tǒng)模型進行了記時假設,而這些假設在實際系統(tǒng)中是無法保證的。
對于第一個點,卷發(fā)哥列了5大點來反駁這個問題,其中一個重要的觀點是他認為雖然 Redlock 沒有提供類似于fencing機制那樣的單調遞增的令牌,但是也有一個隨機串,把這個隨機串當做token,也可以達到同樣的效果啊。當需要和共享資源交互的時候,我們檢查一下這個token是否發(fā)生了變化,如果沒有再執(zhí)行“獲取-修改-寫回”的操作。
最終得出的結論是一個靈魂反問:既然在鎖失效的情況下已經存在一種fencing機制能繼續(xù)保持資源的互斥訪問了,那為什么還要使用一個分布式鎖并且還要求它提供那么強的安全性保證呢?
然而第二個問題,對于網絡延遲或者 GC 暫停,我們前面分析過,對 Redlock 的安全性并不會產生影響,說明卷發(fā)哥在設計的時候其實是考慮過時間因素帶來的問題的。
但是如果是長發(fā)哥提出的時鐘發(fā)生跳躍,很明顯,卷發(fā)哥知道如果時鐘發(fā)生跳躍, Redlock 的安全性就得不到保障,這是他的命門。
但是對于長發(fā)哥寫時鐘跳躍的時候提出的兩個例子:
- 運維人員手動修改了系統(tǒng)時鐘。
- 從NTP服務收到了一個大的時鐘更新事件。
卷發(fā)哥進行了回擊:
第一點這個運維人員手動修改時鐘,屬于人為因素,這個我也沒辦法啊,人家就是要搞你,怎么辦?加強管理,不要這樣做。
第二點從NTP服務收到一個大的時鐘更新,對于這個問題,需要通過運維來保證。需要將大的時間更新到服務器的時候,應當采取少量多次的方式。多次修改,每次更新時間盡量小。
關于這個地方的爭論,就看你是信長發(fā)哥的時間一定會跳躍,還是信卷發(fā)哥的時間跳躍我們也是可以處理的。
關于時鐘跳躍,有一篇文章可以看看,也是這次神仙打架導致的產物:
https://jvns.ca/blog/2016/02/09/til-clock-skew-exists/
文章得出的最終結論是:時鐘跳躍是存在的。
其實我們大家應該都經歷過時鐘跳躍的情況,你還記得2016年的最后一天,當時有個“閏秒”的概念嗎?導致2017年1月1日出現(xiàn)了07:59:60的奇觀。
打架的焦點
經過這樣的一來一回,其實雙方打架的焦點就很明確了,就是大延遲對分布式鎖帶來的影響。
而對于大延遲給Redlock帶來的影響,就是長發(fā)哥分析的那樣,鎖到期了,業(yè)務還沒執(zhí)行完。卷發(fā)哥認為這種影響不單單針對 Redlock ,其他具有自動釋放鎖的分布式鎖也是存在一樣的問題。
而關于大延遲的問題,我在某社交平臺上找到了兩位神仙的下面的對話:
卷發(fā)哥問:我想知道,在我發(fā)文回復之后,我們能否在一點上達成一致,就是大的消息延遲不會給Redlock的運行造成損害。
長發(fā)哥答:對于客戶端和鎖服務器之間的消息延遲,我同意你的觀點。但客戶端和被訪問資源之間的延遲還是有問題的。
所以通過卷發(fā)哥的回擊文章和某社交平臺的記錄,他是同意大的系統(tǒng)時鐘跳躍會造成 Redlock 失效的。在這一點上,他與長發(fā)哥的觀點的不同在于,他認為在實際系統(tǒng)中是可以通過好的運維方式避免大的時鐘跳躍的。
所以到這里,兩位神仙好像又達到了一個平衡,實現(xiàn)了爭論上的求同存異。
打架總結
作為一個互聯(lián)網行業(yè)的從業(yè)者,也是分布式系統(tǒng)的使用者,讀完他們的文章以及由此文章衍生出來的知識點后,受益良多,于是寫下此文作為學習總結,也與大家分享。本文還有很多不足之處,還請各位海涵。
如同文章開篇說的,這場爭論沒有最后的贏家。很明顯卷發(fā)哥是沒有說服長發(fā)哥的,因為在長發(fā)哥2017年出版的《數(shù)據(jù)密集型應用系統(tǒng)設計》一書中,專門有一小節(jié)的名稱叫做:不可靠的時鐘
其實在這場爭論的最后,長發(fā)哥對這場爭論進行了一個非常感性的總結,他說:
下面翻譯來自:
https://www.jianshu.com/p/dd66bdd18a56
對我來說最重要的一點在于:我并不在乎在這場辯論中誰對誰錯 —— 我只關心從其他人的工作中學到的東西,以便我們能夠避免重蹈覆轍,并讓未來更加美好。前人已經為我們創(chuàng)造出了許多偉大的成果:站在巨人的肩膀上,我們得以構建更棒的軟件。
對于任何想法,務必要詳加檢驗,通過論證以及檢查它們是否經得住別人的詳細審查。那是學習過程的一部分。但目標應該是為了獲得知識,而不應該是為了說服別人相信你自己是對的。有時候,那只不過意味著停下來,好好地想一想。
吃瓜網友的收獲
這里的吃瓜網友就是指我啦。
寫這篇文章我的收獲還是挺大的,首先我買了長發(fā)哥的《數(shù)據(jù)密集型應用系統(tǒng)設計》一書,讀了幾節(jié),發(fā)現(xiàn)這書是真的不錯,豆瓣評分9.6,當當最近也在搞活動,52元就能拿下,強推:
廣告數(shù)據(jù)密集型應用系統(tǒng)設計
作者:[美] Martin Kleppmann(馬丁·科勒普曼) 著 譯者:趙軍平 呂云松 耿煜 李三平
其次完成了這周的周更任務,雖然寫的很艱難,從周六中午,寫到周日凌晨3點。。。
然后還吃到了另外的一個瓜,可謂是瓜中瓜。
這周五的時候 Redis 官網不是出現(xiàn)了短暫的宕機嗎,宕機其實也沒啥稀奇的,但是頁面上顯示的是連不上 Redis 。這就有點意思了。
我在寫這篇文章的時候,在卷發(fā)哥的某社交平臺上發(fā)現(xiàn)了這個:
我關心的并不是 OOM,而是卷發(fā)哥居然讓 Redis 官網運行在一臺一個月僅 5 美元,內存只有 1G 的虛擬機上。哈哈哈,震驚,這瓜味道不錯。
最后,由于卷發(fā)哥是個意大利人,由于最近疫情,四川專家組馳援意大利的事,big thank 中國人。其實這個網友的回答挺好的:投桃報李。
疫情早點過去吧,世界和平。
最后說一句(求關注)
我寫到這里的時候,不知不覺已經凌晨3點多了,但是因為一直跟著這兩位大神的激烈討論,我的思維異常的清晰。
寫完之后我也說不出誰對誰錯。我覺得對于系統(tǒng)的設計,每個人的出發(fā)點都不一樣,沒有完美的架構,沒有普適的架構,但是在完美和普適能平衡的很好的架構,就是好的架構。
瞟了一眼文章字數(shù),快突破了1.2w字??赡苡质且黄獙懥藳]人看的勸退文吧,但是沒有關系,只要有一個人看了我的文章覺得有幫助就行。
點個“在看”吧,寫文章很累的,不要白嫖我,需要一點正反饋。
才疏學淺,難免會有紕漏,如果你發(fā)現(xiàn)了錯誤的地方,還請你留言給我指出來,我對其加以修改。(我每篇技術文章都有這句話,我是認真的說的。)