新聞中心
Redis作者和分布式大神打架,最大的贏家竟然是吃瓜網(wǎng)友
作者:why技術(shù) 2020-03-17 09:47:25
新聞
數(shù)據(jù)庫(kù)運(yùn)維
分布式
Redis Redis 作者提出的 Redlock 的解決方案,另一位分布式系統(tǒng)的大神覺(jué)得它不靠譜,于是他們之間開(kāi)始了 battle。

成都創(chuàng)新互聯(lián)公司提供做網(wǎng)站、成都網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì),高端網(wǎng)站設(shè)計(jì),廣告投放等致力于企業(yè)網(wǎng)站建設(shè)與公司網(wǎng)站制作,10年的網(wǎng)站開(kāi)發(fā)和建站經(jīng)驗(yàn),助力企業(yè)信息化建設(shè),成功案例突破上1000家,是您實(shí)現(xiàn)網(wǎng)站建設(shè)的好選擇.
背景鋪墊
面試的時(shí)候,不管你的簡(jiǎn)歷寫沒(méi)寫 Redis,它基本上是一個(gè)繞不過(guò)的話題。
最大的贏家竟然是吃瓜網(wǎng)友">
為了引出本文要討論的關(guān)于 Redlock 的神仙打架的問(wèn)題,我們就得先通過(guò)一個(gè)面試連環(huán)炮:
1.Redis 做分布式鎖的時(shí)候有需要注意的問(wèn)題?
2.如果是 Redis 是單點(diǎn)部署的,會(huì)帶來(lái)什么問(wèn)題?
3.那你準(zhǔn)備怎么解決單點(diǎn)問(wèn)題呢?
4.集群模式下,比如主從模式,有沒(méi)有什么問(wèn)題呢?
5.你知道 Redis 是怎么解決集群模式也不靠譜的問(wèn)題的嗎?
6.那你簡(jiǎn)單的介紹一下 Redlock 吧?
7.你覺(jué)得 Redlock 有什么問(wèn)題呢?
很明顯,上面是一個(gè)常規(guī)的面試連環(huán)套路題。中間還可以插入很多其他的 Redis 的考察點(diǎn),我這里就不做擴(kuò)展了。
單點(diǎn)的 Redis 做分布式鎖不靠譜,導(dǎo)致了基于 Redis 集群模式的分布式鎖解決方案的出現(xiàn)。
基于 Redis 集群模式的分布式鎖解決方案還是不靠譜,Redis 的作者提出了 Redlock 的解決方案。
Redis 作者提出的 Redlock 的解決方案,另一位分布式系統(tǒng)的大神覺(jué)得它不靠譜,于是他們之間開(kāi)始了 battle。
基于這場(chǎng) battle,又引發(fā)了更多的討論。
這場(chǎng) battle 難分伯仲,沒(méi)有最后的贏家。如果一定要選出誰(shuí)是最大的贏家的話,那一定是吃瓜網(wǎng)友。因?yàn)閷?duì)于吃瓜網(wǎng)友來(lái)說(shuō)(比如我),可以從兩位大神的交鋒中學(xué)習(xí)到很多東西。
讓你深刻的體會(huì)到:看起來(lái)那么無(wú)懈可擊的想法,細(xì)細(xì)推敲之下,并不是那么天衣無(wú)縫。
[[318901]]最大的贏家竟然是吃瓜網(wǎng)友">
所以本文就按照下面的五個(gè)模塊展開(kāi)講述。
最大的贏家竟然是吃瓜網(wǎng)友">
先來(lái)一波勸退:本文近1.2w字,謹(jǐn)慎觀看??床幌氯ゲ灰o,點(diǎn)個(gè)贊就是對(duì)于我最大的鼓勵(lì)。奧利給!
[[318903]]最大的贏家竟然是吃瓜網(wǎng)友">
單點(diǎn)Redis
按照我的經(jīng)驗(yàn),當(dāng)面試聊到 Redis 的時(shí)候,百分之 90 的朋友都會(huì)說(shuō)**:Redis在我們的項(xiàng)目中是用來(lái)做熱點(diǎn)數(shù)據(jù)緩存的**。
然后百分之百的面試官都會(huì)問(wèn):
Redis除了拿來(lái)做緩存,你還見(jiàn)過(guò)基于Redis的什么用法?
接下來(lái)百分之 80 的朋友都會(huì)說(shuō)到:我們還用 Redis 做過(guò)分布式鎖。
(當(dāng)然, Redis 除了緩存、分布式鎖之外還有非常非常多的奇技淫巧,不是本文重點(diǎn),大家有興趣的可以自己去了解一下。)
那么面試官就會(huì)接著說(shuō):
那你給我描述(或者寫一下偽代碼)基于Redis的加鎖和釋放鎖的細(xì)節(jié)吧。
注意面試官這里說(shuō)的是加鎖和釋放鎖的細(xì)節(jié),魔鬼都在細(xì)節(jié)里。
問(wèn)這個(gè)問(wèn)題面試官無(wú)非是想要聽(tīng)到下面幾個(gè)關(guān)鍵點(diǎn):
關(guān)鍵點(diǎn)一:原子命令加鎖。因?yàn)橛械摹澳昃檬蕖钡奈恼轮袑?duì)于 Redis 的加鎖操作是先set key,再設(shè)置 key 的過(guò)期時(shí)間。這樣寫的根本原因是在早期的 Redis 版本中并不支持原子命令加鎖的操作。不是原子操作會(huì)帶來(lái)什么問(wèn)題,就不用我說(shuō)了吧?如果你不知道,你先回去等通知吧。
而在 2.6.12 版本后,可以通過(guò)向 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ī)字符串,它要保證在足夠長(zhǎng)的一段時(shí)間內(nèi)在所有客戶端的所有獲取鎖的請(qǐng)求中都是唯一的。
NX:表示只有當(dāng)要設(shè)置的 key 值不存在的時(shí)候才能 set 成功。這保證了只有第一個(gè)請(qǐng)求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無(wú)法獲得鎖。
PX 30000:表示這個(gè)鎖有一個(gè) 30 秒的自動(dòng)過(guò)期時(shí)間。當(dāng)然,這里 30 秒只是一個(gè)例子,客戶端可以選擇合適的過(guò)期時(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è)問(wèn)題了。常規(guī)熱身送分題呀,朋友們,得記住了。)
[[318904]]最大的贏家竟然是吃瓜網(wǎng)友">
集群模式
面試官就會(huì)接著問(wèn)了:
經(jīng)過(guò)剛剛的討論,我們已經(jīng)有較好的方法獲取鎖和釋放鎖。基于Redis單實(shí)例,假設(shè)這個(gè)單實(shí)例總是可用,這種方法已經(jīng)足夠安全。如果這個(gè)Redis節(jié)點(diǎn)掛掉了呢?
到這個(gè)問(wèn)題其實(shí)可以直接聊到 Redlock 了。但是你別慌啊,為了展示你豐富的知識(shí)儲(chǔ)備(瘋狂的刷題準(zhǔn)備),你得先自己聊一聊 Redis 的集群,你可以這樣去說(shuō):
[[318905]]最大的贏家竟然是吃瓜網(wǎng)友">
為了避免節(jié)點(diǎn)掛掉導(dǎo)致的問(wèn)題,我們可以采用Redis集群的方法來(lái)實(shí)現(xiàn)Redis的高可用。
Redis集群方式共有三種:主從模式,哨兵模式,cluster(集群)模式
其中主從模式會(huì)保證數(shù)據(jù)在從節(jié)點(diǎn)還有一份,但是主節(jié)點(diǎn)掛了之后,需要手動(dòng)把從節(jié)點(diǎn)切換為主節(jié)點(diǎn)。它非常簡(jiǎ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),可以說(shuō)很多道道出來(lái),由于不是本文重點(diǎn)就不詳細(xì)描述了。主要表達(dá)的意思是你得在面試的時(shí)候遇到相關(guān)問(wèn)題,需要展示自己是知道這些東西的,都是面試的套路。)
在上面描述的集群模式下還是會(huì)出現(xiàn)一個(gè)問(wèn)題,由于節(jié)點(diǎn)之間是采用異步通信的方式。如果剛剛在 Master 節(jié)點(diǎn)上加了鎖,但是數(shù)據(jù)還沒(méi)被同步到 Salve。這時(shí) Master 節(jié)點(diǎn)掛了,它上面的鎖就沒(méi)了,等新的 Master 出來(lái)后(主從模式的手動(dòng)切換或者哨兵模式的一次 failover 的過(guò)程),就可以再次獲取同樣的鎖,出現(xiàn)一把鎖被拿到了兩次的場(chǎng)景。
鎖都被拿了兩次了,也就不滿足安全性了。一個(gè)安全的鎖,不管是不是分布式的,在任意一個(gè)時(shí)刻,都只有一個(gè)客戶端持有。
[[318906]]最大的贏家竟然是吃瓜網(wǎng)友">
Redlock簡(jiǎn)介
為了解決上面的問(wèn)題,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)過(guò)下面的五個(gè)步驟:
步驟描述來(lái)源: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ù)器端沒(méi)有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試另外一個(gè) Redis 實(shí)例。
3.客戶端使用當(dāng)前時(shí)間減去開(kāi)始獲取鎖時(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)槟承┰?,獲取鎖失?。](méi)有在至少 N/2+1 個(gè)Redis實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過(guò)了有效時(shí)間),客戶端應(yīng)該在所有的 Redis 實(shí)例上進(jìn)行解鎖(即便某些 Redis 實(shí)例根本就沒(méi)有加鎖成功)。
通過(guò)上面的步驟我們可以知道,只要大多數(shù)的節(jié)點(diǎn)可以正常工作,就可以保證 Redlock 的正常工作。這樣就可以解決前面單點(diǎn) Redis 的情況下我們討論的節(jié)點(diǎn)掛掉,由于異步通信,導(dǎo)致鎖失效的問(wèn)題。
但是,還是不能解決故障重啟后帶來(lái)的鎖的安全性的問(wèn)題。你想一下下面這個(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 上的鎖沒(méi)有持久化下來(lái)。 客戶端 2 發(fā)起申請(qǐng)同一把鎖的操作,在 B,C 上加鎖成功。
3.這個(gè)時(shí)候就又出現(xiàn)同一把鎖,同時(shí)被客戶端 1 和客戶端 2 所持有了。
(接下來(lái)又得說(shuō)一說(shuō)Redis的持久化策略了,全是知識(shí)點(diǎn)啊,朋友們)
[[318907]]最大的贏家竟然是吃瓜網(wǎng)友">
比如,Redis 的 AOF 持久化方式默認(rèn)情況下是每秒寫一次磁盤,即 fsync 操作,因此最壞的情況下可能丟失 1 秒的數(shù)據(jù)。
當(dāng)然,你也可以設(shè)置成每次修改數(shù)據(jù)都進(jìn)行 fsync 操作(fsync=always),但這會(huì)嚴(yán)重降低 Redis 的性能,違反了它的設(shè)計(jì)理念。(我也沒(méi)見(jiàn)過(guò)這樣用的,可能還是見(jiàn)的太少了吧。)
而且,你以為執(zhí)行了 fsync 就不會(huì)丟失數(shù)據(jù)了?天真,真實(shí)的系統(tǒng)環(huán)境是復(fù)雜的,這都已經(jīng)脫離 Redis 的范疇了。上升到服務(wù)器、系統(tǒng)問(wèn)題了。
[[318908]]最大的贏家竟然是吃瓜網(wǎng)友">
所以,根據(jù)墨菲定律,上面舉的例子:由于節(jié)點(diǎn)重啟引發(fā)的鎖失效問(wèn)題,總是有可能出現(xiàn)的。
為了解決這一問(wèn)題,Redis 的作者又提出了延遲重啟(delayed restarts)的概念。
最大的贏家竟然是吃瓜網(wǎng)友">
意思就是說(shuō),一個(gè)節(jié)點(diǎn)崩潰后,不要立即重啟它,而是等待一定的時(shí)間后再重啟。等待的時(shí)間應(yīng)該大于鎖的過(guò)期時(shí)間(TTL)。這樣做的目的是保證這個(gè)節(jié)點(diǎn)在重啟前所參與的鎖都過(guò)期。相當(dāng)于把以前的帳勾銷之后才能參與后面的加鎖操作。
但是有個(gè)問(wèn)題就是:在等待的時(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)任何鎖都將無(wú)法加鎖成功。
Redlock 算法還有一個(gè)需要注意的點(diǎn)是它的釋放鎖操作。
釋放鎖的時(shí)候是要向所有節(jié)點(diǎn)發(fā)起釋放鎖的操作的。這樣做的目的是為了解決有可能在加鎖階段,這個(gè)節(jié)點(diǎn)收到加鎖請(qǐng)求了,也set成功了,但是由于返回給客戶端的響應(yīng)包丟了,導(dǎo)致客戶端以為沒(méi)有加鎖成功。所以,釋放鎖的時(shí)候要向所有節(jié)點(diǎn)發(fā)起釋放鎖的操作。
你可能覺(jué)得這不是常規(guī)操作嗎?
有的細(xì)節(jié)就是這樣,說(shuō)出來(lái)后覺(jué)得不過(guò)如此,但是有可能自己就是想不到這個(gè)點(diǎn),導(dǎo)致問(wèn)題的出現(xiàn),所以我們才會(huì)說(shuō):細(xì)節(jié),魔鬼都在細(xì)節(jié)里。
好了,簡(jiǎn)介大概就說(shuō)到這里,有興趣的朋友可以再去看看官網(wǎng),補(bǔ)充一下。
中文:http://redis.cn/topics/distlock.html英文:https://redis.io/topics/distlock
好了,經(jīng)過(guò)這么長(zhǎng),這么長(zhǎng)的鋪墊,我們終于可以進(jìn)入到神仙打架環(huán)節(jié)。
神仙打架
神仙一:Redis 的作者 antirez 。有的朋友對(duì)英文名字不太敏感,所以后面我就叫他卷發(fā)哥吧。
[[318909]]最大的贏家竟然是吃瓜網(wǎng)友">
神仙二:分布式領(lǐng)域?qū)<?Martin Kleppmann,我們叫他長(zhǎng)發(fā)哥吧。
[[318910]]最大的贏家竟然是吃瓜網(wǎng)友">
看完上面兩位神仙的照片,再看看我為了寫這篇文章又日漸稀少的頭發(fā),我忍不住哭出聲來(lái)??赡苤挥薪o我點(diǎn)贊,才能平復(fù)我的心情吧。
卷發(fā)哥在官網(wǎng)介紹 Redlock 頁(yè)面的最后寫到:如果你也是使用分布式系統(tǒng)的人員,你的觀點(diǎn)和意見(jiàn)非常重要,歡迎和我們討論。
最大的贏家竟然是吃瓜網(wǎng)友">
于是,“求錘得錘”!這一錘,錘出了眾多的吃瓜網(wǎng)友,其中不乏在相關(guān)領(lǐng)域的專業(yè)人士。
[[318911]]最大的贏家竟然是吃瓜網(wǎng)友">
長(zhǎng)發(fā)哥出錘
故事得從 2016年2月8號(hào) 長(zhǎng)發(fā)哥發(fā)布的一篇文章《How to do distributed locking》說(shuō)起:
文章地址:http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
這一部分直接翻譯過(guò)來(lái)就是:
作為本書(《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》)研究的一部分,我在Redis網(wǎng)站上 看到了一種稱為Redlock的算法。該算法聲稱在Redis實(shí)現(xiàn)容錯(cuò)的分布式鎖(或更確切地說(shuō), 租約),并且該頁(yè)面要求來(lái)自分布式系統(tǒng)人員的反饋。這個(gè)算法讓我產(chǎn)生了一些思考,因此我花了一些時(shí)間寫了我的這篇文章。由于Redlock已經(jīng)有10多個(gè)獨(dú)立的實(shí)現(xiàn),而且我們不知道誰(shuí)已經(jīng)在依賴此算法,因此我認(rèn)為值得公開(kāi)分享我的筆記。我不會(huì)討論Redis的其他方面,其中一些已經(jīng)在其他地方受到了批評(píng) 。
你看這個(gè)文章,開(kāi)頭就是火藥味十足:你說(shuō)要反饋,那我就給你反饋。而且你這個(gè)東西有其他問(wèn)題,我也就不說(shuō)了。(其實(shí)作者在這篇文章中也說(shuō)了,他很喜歡并且也在使用 Redis,只是他覺(jué)得這個(gè) Redlock 算法是不嚴(yán)謹(jǐn)?shù)模?/p>
長(zhǎng)發(fā)哥主要圍繞了下面的這張圖進(jìn)行了展開(kāi):
最大的贏家竟然是吃瓜網(wǎng)友">
要是一眼沒(méi)看明白,我再給你一個(gè)中文版的,來(lái)自長(zhǎng)發(fā)哥于2017年出版的書《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》:
最大的贏家竟然是吃瓜網(wǎng)友">
可以看到上面的圖片中提到了申請(qǐng)租約、租約到期的關(guān)鍵詞,租約其實(shí)就是可以理解為帶超時(shí)時(shí)間的鎖。
而在書中,這張圖片的下面寫的描述這樣的,你咂摸咂摸:
最大的贏家竟然是吃瓜網(wǎng)友">
拿 HBase 舉例,其設(shè)計(jì)的目標(biāo)是確保存儲(chǔ)系統(tǒng)的文件一次只能由一個(gè)客戶端訪問(wèn),如果多個(gè)客戶端試圖同時(shí)寫入該文件,文件就會(huì)被破壞。那么上面的圖片解釋起來(lái)就是:
1.客戶端 1 先去申請(qǐng)鎖,并且成功獲取到鎖。之后客戶端進(jìn)行了長(zhǎng)時(shí)間的 GC 導(dǎo)致了 STW 的情況。
2.在 STW 期間,客戶端 1 獲取的鎖的超時(shí)時(shí)間到了,鎖也就失效了。
3.由于客戶端 1 的鎖已經(jīng)過(guò)期失效了,所以客戶端 2 去申請(qǐng)鎖就可以成功獲得鎖。
4.客戶端 2 開(kāi)始寫文件,并完成文件的寫入。
5.客戶端 1 從 STW 中恢復(fù)過(guò)來(lái),他并不知道自己的鎖過(guò)期了,還是會(huì)繼續(xù)執(zhí)行文件寫入操作,導(dǎo)致客戶端 2 寫入的文件被破壞。而且可以看到,它沒(méi)有滿足鎖在任意時(shí)刻只有一個(gè)客戶端持有的原則,即沒(méi)有滿足互斥性。
書里面沒(méi)有明說(shuō),但是你品一品,這里的鎖服務(wù)難道不是在說(shuō) Redis?
有的朋友就會(huì)說(shuō)了,那客戶端 1 寫入文件的時(shí)候,再判斷一下自己的鎖有沒(méi)有過(guò)期不就可以了嗎?
你可真是個(gè)小機(jī)靈鬼呢,那我問(wèn)你,GC 可能是發(fā)生在任何時(shí)間的,萬(wàn)一 GC 發(fā)生在判斷之后呢?
你繼續(xù)懟我,如果客戶端使用的是沒(méi)有 GC 的語(yǔ)言呢?
[[318913]]最大的贏家竟然是吃瓜網(wǎng)友">
**GC 不是導(dǎo)致線程暫停的唯一原因啊,朋友們。**發(fā)生這種情況的原因有很多的,你看看長(zhǎng)發(fā)哥書里舉的例子:
最大的贏家竟然是吃瓜網(wǎng)友">
上面的內(nèi)容總結(jié)起來(lái),就是就算鎖服務(wù)是正常的,但是由于鎖是有持有時(shí)間的,由于客戶端阻塞、長(zhǎng)時(shí)間的 GC 或者網(wǎng)絡(luò)原因,導(dǎo)致共享資源被一個(gè)以上的客戶端同時(shí)訪問(wèn)了。
其實(shí)上面長(zhǎng)發(fā)哥在書里直接說(shuō)了:這是不正確的實(shí)現(xiàn)。
你多品一品,上面的圖是不是有點(diǎn)像由于 Redis 鎖的過(guò)期時(shí)間設(shè)置的不合理,導(dǎo)致前一個(gè)任務(wù)還沒(méi)執(zhí)行完成,但是鎖的時(shí)間到期了,后一個(gè)任務(wù)也申請(qǐng)到了鎖。
對(duì)于這種場(chǎng)景,Redission 其實(shí)有自己的看門狗機(jī)制。但是不在這次 Redlock 的討論范圍內(nèi),所以這里就不描述了。
長(zhǎng)發(fā)哥提出的解決方案是什么呢?
他稱為:fencing token。
長(zhǎng)發(fā)哥認(rèn)為使用鎖和租約機(jī)制來(lái)保護(hù)資源的并發(fā)訪問(wèn)時(shí),必須確保因?yàn)楫惓T颍瑢?dǎo)致鎖過(guò)期的那個(gè)節(jié)點(diǎn)不能影響其他正常的部分,要實(shí)現(xiàn)這一目標(biāo),可以采用一直相當(dāng)簡(jiǎn)單的 fencing(柵欄)。
[[318914]]最大的贏家竟然是吃瓜網(wǎng)友">
假設(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)處理過(guò)更高令牌的請(qǐng)求,則拒絕執(zhí)行該請(qǐng)求。
比如下面的圖片:
最大的贏家竟然是吃瓜網(wǎng)友">
1.客戶端 1 獲得一個(gè)具有超時(shí)時(shí)間的鎖的同時(shí)得到了令牌號(hào) 33,但隨后陷入了一個(gè)長(zhǎng)時(shí)間的暫停直到鎖到期。
2.這時(shí)客戶端2已經(jīng)獲得了鎖和令牌號(hào) 34 ,然后發(fā)送寫請(qǐng)求(以及令牌號(hào) 34 )到存儲(chǔ)服務(wù)。
3.接下來(lái)客戶端 1 恢復(fù)過(guò)來(lái),并以令牌號(hào) 33 來(lái)嘗試寫入,存儲(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 來(lái)充當(dāng) fencing 令牌,這兩個(gè)都可以滿足單調(diào)遞增的要求。
在長(zhǎng)發(fā)哥的這種機(jī)制中,實(shí)際上就是要求資源本身必須主動(dòng)檢查請(qǐng)求所持令牌信息,如果發(fā)現(xiàn)已經(jīng)處理過(guò)更高令牌的請(qǐng)求,要拒絕持有低令牌的所有寫請(qǐng)求。
但是,不是所有的資源都是數(shù)據(jù)庫(kù)里面的數(shù)據(jù),我們可以通過(guò)版本號(hào)去支持額外的令牌檢查的,那么對(duì)于不支持額外的令牌檢查資源,我們也可以借助這種思想繞過(guò)這個(gè)限制,比如對(duì)于訪問(wèn)文件存儲(chǔ)服務(wù)的情況,我們可以將令牌嵌入到文件名中。
總之,為了避免在鎖保護(hù)之外發(fā)生請(qǐng)求處理,需要進(jìn)行額外的檢查機(jī)制。
長(zhǎng)發(fā)哥在書中也說(shuō)到了:在服務(wù)端檢查令牌可能看起來(lái)有點(diǎn)復(fù)雜,但是這其實(shí)是推薦的正確的做法:系統(tǒng)服務(wù)不能假定所有的客戶端都表現(xiàn)的符合預(yù)期。從安全角度講,服務(wù)端必須防范這種來(lái)自客戶端的濫用。
這個(gè)就類似于我們作為后端開(kāi)發(fā)人員,也不能相信來(lái)自前端或者其他接口過(guò)來(lái)的數(shù)據(jù),必須對(duì)其進(jìn)行校驗(yàn)。
到這里長(zhǎng)發(fā)哥鋪墊完成了,開(kāi)始轉(zhuǎn)頭指向 RedLock,他認(rèn)為 Redlock 是一個(gè)嚴(yán)重依賴系統(tǒng)時(shí)鐘的分布式鎖。
最大的贏家竟然是吃瓜網(wǎng)友">
他舉了一個(gè)例子:
1.客戶端 1 從 Redis 節(jié)點(diǎn) A, B, C 成功獲取了鎖。由于網(wǎng)絡(luò)問(wèn)題,無(wú)法訪問(wèn) D 和 E。
2.節(jié)點(diǎn) C 上的時(shí)鐘發(fā)生了向前跳躍,導(dǎo)致它上面維護(hù)的鎖過(guò)期了。
3.客戶端 2 從 Redis 節(jié)點(diǎn) C, D, E 成功獲取了同一個(gè)資源的鎖。由于網(wǎng)絡(luò)問(wèn)題,無(wú)法訪問(wèn) 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)確了,那么該算法的安全性也就得不到保障了。
長(zhǎng)發(fā)哥舉這個(gè)例子其實(shí)是為了輔佐他前面提出的觀點(diǎn):一個(gè)好的分布式算法應(yīng)該是基于異步模型的,算法的安全性不應(yīng)該依賴與任何記時(shí)假設(shè),就是不能把時(shí)間作為安全保障的。在異步模型中,程序暫停、消息在網(wǎng)絡(luò)中延遲甚至丟失、系統(tǒng)時(shí)間錯(cuò)誤這些因素都不應(yīng)該影響它的安全性,只能影響到它的活性。
用大白話說(shuō),就是在極其極端的情況下,分布式系統(tǒng)頂天了也就是在有限的時(shí)間內(nèi)不能給出結(jié)果而已,而不能給出一個(gè)錯(cuò)誤的結(jié)果。
這樣的算法實(shí)際上是存在的,比如 Paxos、Raft。很明顯,按照這個(gè)標(biāo)準(zhǔn), Redlock 的安全級(jí)別是不夠的。
而對(duì)于卷發(fā)哥提出的延遲啟動(dòng)方案,長(zhǎng)發(fā)哥還是一棒子打死:你延遲啟動(dòng)咋的?延遲啟動(dòng)還不是依賴于合理準(zhǔn)確的時(shí)間度量。
[[318915]]最大的贏家竟然是吃瓜網(wǎng)友">
可能是長(zhǎng)發(fā)哥覺(jué)得舉這個(gè)時(shí)鐘跳躍的例子不夠好的,大家都可能認(rèn)為時(shí)鐘跳躍是不現(xiàn)實(shí)的,因?yàn)閷?duì)正確配置NTP就能擺正時(shí)鐘非常有信心。
在這種情況下,他舉了一個(gè)進(jìn)程暫??赡軐?dǎo)致算法失敗的示例:
最大的贏家竟然是吃瓜網(wǎng)友">
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)入了長(zhǎng)時(shí)間的 GC 階段。
3.長(zhǎng)時(shí)間的 GC,導(dǎo)致在所有的 Redis 節(jié)點(diǎn)上,鎖過(guò)期了。
4.客戶端 2 在 A, B, C, D, E 上申請(qǐng)并獲取到了鎖。
5.客戶端 1 從 GC 階段中恢復(fù),收到了前面第 2 步來(lái)自各個(gè) Redis 節(jié)點(diǎn)的請(qǐng)求結(jié)果。客戶端 1 認(rèn)為自己成功獲取到了鎖。
6.客戶端 1 和客戶端 2 現(xiàn)在都認(rèn)為自己持有了鎖。
其實(shí)只要十分清楚 Redlock 的加鎖過(guò)程,我們就知道,這種情況其實(shí)對(duì)于 Redlock 是沒(méi)有影響的,因?yàn)樵诘?5 步,客戶端 1 從 GC 階段中恢復(fù)過(guò)來(lái)以后,在 Redlock 算法中,(我們前面 Redlock 簡(jiǎn)介的時(shí)候提到的第四步)如果取到了鎖,key 的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間。
所以客戶端1通過(guò)這個(gè)檢查發(fā)現(xiàn)鎖已經(jīng)過(guò)期了,不會(huì)再認(rèn)為自己成功獲取到鎖了。
而隨后卷發(fā)哥的回?fù)糁幸蔡岬搅诉@點(diǎn)。
[[318916]]最大的贏家竟然是吃瓜網(wǎng)友">
但是,細(xì)細(xì)想來(lái),我覺(jué)得長(zhǎng)發(fā)哥的意圖不在于此。拋開(kāi)上面的問(wèn)題來(lái)講,他更想突出的是,一個(gè)鎖在客戶端拿到后,還沒(méi)使用就過(guò)期了,這是不好的。從客戶端的角度來(lái)看,就是這玩意不靠譜啊,你給我一把鎖,我還沒(méi)用呢,你就過(guò)期了?
[[318917]]最大的贏家竟然是吃瓜網(wǎng)友">
除了上面說(shuō)的這些點(diǎn)外,長(zhǎng)發(fā)哥還提出了一個(gè)算是自己的經(jīng)驗(yàn)之談吧:
我們獲取鎖的用途是什么?
在他看來(lái)不外乎兩個(gè)方面,效率和正確性。他分別描述如下:
最大的贏家竟然是吃瓜網(wǎng)友">
如果是為了效率,那么就是要協(xié)調(diào)各個(gè)客戶端,避免他們做重復(fù)的工作。這種場(chǎng)景下,即使鎖偶爾失效了,只是可能出現(xiàn)兩個(gè)客戶端完成了同樣的工作,其結(jié)果是成本略有增加(您最終向 AWS 支付的費(fèi)用比原本多5美分),或者帶來(lái)不便(例如,用戶最終兩次收到相同的電子郵件通知)。
如果是為了正確性,那么在任何情況下都不允許鎖失效的情況發(fā)生,因?yàn)橐坏┌l(fā)生,就可能意味著數(shù)據(jù)不一致,數(shù)據(jù)丟失,文件損壞,或者其它嚴(yán)重的問(wèn)題。(比如個(gè)患者注射了兩倍的藥劑)
最后,長(zhǎng)發(fā)哥得出的結(jié)論是:neither fish nor fowl(不倫不類)
對(duì)于提升效率的場(chǎng)景下,使用分布式鎖,允許鎖的偶爾失效,那么使用單 Redis 節(jié)點(diǎn)的鎖方案就足夠了,簡(jiǎn)單而且效率高。用 Redlock 太重。
對(duì)于正確性要求高的場(chǎng)景下,它是依賴于時(shí)間的,不是一個(gè)足夠強(qiáng)的算法。Redlock并沒(méi)有保住正確性。
最大的贏家竟然是吃瓜網(wǎng)友">
那應(yīng)該使用什么技術(shù)呢?
長(zhǎng)發(fā)哥認(rèn)為,應(yīng)該考慮類似 Zookeeper 的方案,或者支持事務(wù)的數(shù)據(jù)庫(kù)。
最大的贏家竟然是吃瓜網(wǎng)友">
卷發(fā)哥回?fù)?/h2>
長(zhǎng)發(fā)哥發(fā)出《How to do distributed locking》這篇文章的第二天,卷發(fā)哥就進(jìn)行了回?fù)?,發(fā)布了名為《Is Redlock safe?》的文章。
文章地址:http://antirez.com/news/101
要說(shuō)大佬不愧是大佬,卷發(fā)哥的回?fù)魲l理清楚,行文流暢。他總結(jié)后認(rèn)為長(zhǎng)發(fā)哥覺(jué)得 Redlock 不安全主要分為兩個(gè)方面:
最大的贏家竟然是吃瓜網(wǎng)友">
1.帶有自動(dòng)過(guò)期功能的分布式鎖,需要一種方法(fencing機(jī)制)來(lái)避免客戶端在過(guò)期時(shí)間后使用鎖時(shí)出現(xiàn)問(wèn)題,從而對(duì)共享資源進(jìn)行真正的互斥保護(hù)。長(zhǎng)發(fā)哥說(shuō)Redlock沒(méi)有這種機(jī)制。
2.長(zhǎng)發(fā)哥說(shuō),無(wú)論問(wèn)題“1”如何解決,該算法本質(zhì)上都是不安全的,因?yàn)樗鼘?duì)系統(tǒng)模型進(jìn)行了記時(shí)假設(shè),而這些假設(shè)在實(shí)際系統(tǒng)中是無(wú)法保證的。
對(duì)于第一個(gè)點(diǎn),卷發(fā)哥列了5大點(diǎn)來(lái)反駁這個(gè)問(wèn)題,其中一個(gè)重要的觀點(diǎn)是他認(rèn)為雖然 Redlock 沒(méi)有提供類似于fencing機(jī)制那樣的單調(diào)遞增的令牌,但是也有一個(gè)隨機(jī)串,把這個(gè)隨機(jī)串當(dāng)做token,也可以達(dá)到同樣的效果啊。當(dāng)需要和共享資源交互的時(shí)候,我們檢查一下這個(gè)token是否發(fā)生了變化,如果沒(méi)有再執(zhí)行“獲取-修改-寫回”的操作。
最大的贏家竟然是吃瓜網(wǎng)友">
最終得出的結(jié)論是一個(gè)靈魂反問(wèn):既然在鎖失效的情況下已經(jīng)存在一種fencing機(jī)制能繼續(xù)保持資源的互斥訪問(wèn)了,那為什么還要使用一個(gè)分布式鎖并且還要求它提供那么強(qiáng)的安全性保證呢?
最大的贏家竟然是吃瓜網(wǎng)友">
[[318918]]最大的贏家竟然是吃瓜網(wǎng)友">
然而第二個(gè)問(wèn)題,對(duì)于網(wǎng)絡(luò)延遲或者 GC 暫停,我們前面分析過(guò),對(duì) Redlock 的安全性并不會(huì)產(chǎn)生影響,說(shuō)明卷發(fā)哥在設(shè)計(jì)的時(shí)候其實(shí)是考慮過(guò)時(shí)間因素帶來(lái)的問(wèn)題的。
但是如果是長(zhǎng)發(fā)哥提出的時(shí)鐘發(fā)生跳躍,很明顯,卷發(fā)哥知道如果時(shí)鐘發(fā)生跳躍, Redlock 的安全性就得不到保障,這是他的命門。
但是對(duì)于長(zhǎng)發(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è)我也沒(méi)辦法啊,人家就是要搞你,怎么辦?加強(qiáng)管理,不要這樣做。
第二點(diǎn)從NTP服務(wù)收到一個(gè)大的時(shí)鐘更新,對(duì)于這個(gè)問(wèn)題,需要通過(guò)運(yùn)維來(lái)保證。需要將大的時(shí)間更新到服務(wù)器的時(shí)候,應(yīng)當(dāng)采取少量多次的方式。多次修改,每次更新時(shí)間盡量小。
關(guān)于這個(gè)地方的爭(zhēng)論,就看你是信長(zhǎng)發(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í)鐘跳躍是存在的。
最大的贏家竟然是吃瓜網(wǎng)友">
其實(shí)我們大家應(yīng)該都經(jīng)歷過(guò)時(shí)鐘跳躍的情況,你還記得2016年的最后一天,當(dāng)時(shí)有個(gè)“閏秒”的概念嗎?導(dǎo)致2017年1月1日出現(xiàn)了07:59:60的奇觀。
最大的贏家竟然是吃瓜網(wǎng)友">
打架的焦點(diǎn)
經(jīng)過(guò)這樣的一來(lái)一回,其實(shí)雙方打架的焦點(diǎn)就很明確了,就是大延遲對(duì)分布式鎖帶來(lái)的影響。
而對(duì)于大延遲給Redlock帶來(lái)的影響,就是長(zhǎng)發(fā)哥分析的那樣,鎖到期了,業(yè)務(wù)還沒(méi)執(zhí)行完。卷發(fā)哥認(rèn)為這種影響不單單針對(duì) Redlock ,其他具有自動(dòng)釋放鎖的分布式鎖也是存在一樣的問(wèn)題。
最大的贏家竟然是吃瓜網(wǎng)友">
而關(guān)于大延遲的問(wèn)題,我在某社交平臺(tái)上找到了兩位神仙的下面的對(duì)話:
最大的贏家竟然是吃瓜網(wǎng)友">
卷發(fā)哥問(wèn):我想知道,在我發(fā)文回復(fù)之后,我們能否在一點(diǎn)上達(dá)成一致,就是大的消息延遲不會(huì)給Redlock的運(yùn)行造成損害。
長(zhǎng)發(fā)哥答:對(duì)于客戶端和鎖服務(wù)器之間的消息延遲,我同意你的觀點(diǎn)。但客戶端和被訪問(wèn)資源之間的延遲還是有問(wèn)題的。
所以通過(guò)卷發(fā)哥的回?fù)粑恼潞湍成缃黄脚_(tái)的記錄,他是同意大的系統(tǒng)時(shí)鐘跳躍會(huì)造成 Redlock 失效的。在這一點(diǎn)上,他與長(zhǎng)發(fā)哥的觀點(diǎn)的不同在于,他認(rèn)為在實(shí)際系統(tǒng)中是可以通過(guò)好的運(yùn)維方式避免大的時(shí)鐘跳躍的。
[[318919]]最大的贏家竟然是吃瓜網(wǎng)友">
所以到這里,兩位神仙好像又達(dá)到了一個(gè)平衡,實(shí)現(xiàn)了爭(zhēng)論上的求同存異。
打架總結(jié)
作為一個(gè)互聯(lián)網(wǎng)行業(yè)的從業(yè)者,也是分布式系統(tǒng)的使用者,讀完他們的文章以及由此文章衍生出來(lái)的知識(shí)點(diǎn)后,受益良多,于是寫下此文作為學(xué)習(xí)總結(jié),也與大家分享。本文還有很多不足之處,還請(qǐng)各位海涵。
如同文章開(kāi)篇說(shuō)的,這場(chǎng)爭(zhēng)論沒(méi)有最后的贏家。很明顯卷發(fā)哥是沒(méi)有說(shuō)服長(zhǎng)發(fā)哥的,因?yàn)樵陂L(zhǎng)發(fā)哥2017年出版的《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》一書中,專門有一小節(jié)的名稱叫做:不可靠的時(shí)鐘
最大的贏家竟然是吃瓜網(wǎng)友">
其實(shí)在這場(chǎng)爭(zhēng)論的最后,長(zhǎng)發(fā)哥對(duì)這場(chǎng)爭(zhēng)論進(jìn)行了一個(gè)非常感性的總結(jié),他說(shuō):
下面翻譯來(lái)自:https://www.jianshu.com/p/dd66bdd18a56
對(duì)我來(lái)說(shuō)最重要的一點(diǎn)在于:我并不在乎在這場(chǎng)辯論中誰(shuí)對(duì)誰(shuí)錯(cuò) —— 我只關(guān)心從其他人的工作中學(xué)到的東西,以便我們能夠避免重蹈覆轍,并讓未來(lái)更加美好。前人已經(jīng)為我們創(chuàng)造出了許多偉大的成果:站在巨人的肩膀上,我們得以構(gòu)建更棒的軟件。
對(duì)于任何想法,務(wù)必要詳加檢驗(yàn),通過(guò)論證以及檢查它們是否經(jīng)得住別人的詳細(xì)審查。那是學(xué)習(xí)過(guò)程的一部分。但目標(biāo)應(yīng)該是為了獲得知識(shí),而不應(yīng)該是為了說(shuō)服別人相信你自己是對(duì)的。有時(shí)候,那只不過(guò)意味著停下來(lái),好好地想一想。
[[318921]]最大的贏家竟然是吃瓜網(wǎng)友">
吃瓜網(wǎng)友的收獲
這里的吃瓜網(wǎng)友就是指我啦。
寫這篇文章我的收獲還是挺大的,首先我買了長(zhǎ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í)也沒(méi)啥稀奇的,但是頁(yè)面上顯示的是連不上 Redis 。這就有點(diǎn)意思了。
最大的贏家竟然是吃瓜網(wǎng)友">
我在寫這篇文章的時(shí)候,在卷發(fā)哥的某社交平臺(tái)上發(fā)現(xiàn)了這個(gè):
最大的贏家竟然是吃瓜網(wǎng)友">
我關(guān)心的并不是 OOM,而是卷發(fā)哥居然讓 Redis 官網(wǎng)運(yùn)行在一臺(tái)一個(gè)月僅 5 美元,內(nèi)存只有 1G 的虛擬機(jī)上。哈哈哈,震驚,這瓜味道不錯(cuò)。
最后,由于卷發(fā)哥是個(gè)意大利人,由于最近疫情,四川專家組馳援意大利的事,big thank 中國(guó)人。其實(shí)這個(gè)網(wǎng)友的回答挺好的:投桃報(bào)李。
最大的贏家竟然是吃瓜網(wǎng)友">
疫情早點(diǎn)過(guò)去吧,世界和平。
最后說(shuō)一句
我寫到這里的時(shí)候,不知不覺(jué)已經(jīng)凌晨3點(diǎn)多了,但是因?yàn)橐恢备@兩位大神的激烈討論,我的思維異常的清晰。
寫完之后我也說(shuō)不出誰(shuí)對(duì)誰(shuí)錯(cuò)。我覺(jué)得對(duì)于系統(tǒng)的設(shè)計(jì),每個(gè)人的出發(fā)點(diǎn)都不一樣,沒(méi)有完美的架構(gòu),沒(méi)有普適的架構(gòu),但是在完美和普適能平衡的很好的架構(gòu),就是好的架構(gòu)。
本文標(biāo)題:Redis作者和分布式大神打架,最大的贏家竟然是吃瓜網(wǎng)友
轉(zhuǎn)載源于:http://www.dlmjj.cn/article/dhieedp.html


咨詢
建站咨詢
