<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深度剖析:Redis分布式鎖到底安全嗎?看完這篇文章徹底懂了!

          共 14895字,需瀏覽 30分鐘

           ·

          2021-06-16 11:50

          ????????????閱讀本文大約需要 20 分鐘。

          這篇文章我想和你聊一聊,關(guān)于 Redis 分布式鎖的「安全性」問題。

          Redis 分布式鎖的話題,很多文章已經(jīng)寫爛了,我為什么還要寫這篇文章呢?

          因?yàn)槲野l(fā)現(xiàn)網(wǎng)上 99% 的文章,并沒有把這個問題真正講清楚。導(dǎo)致很多讀者看了很多文章,依舊云里霧里。例如下面這些問題,你能清晰地回答上來嗎?

          • 基于 Redis 如何實(shí)現(xiàn)一個分布式鎖?
          • Redis 分布式鎖真的安全嗎?
          • Redis 的 Redlock 有什么問題?一定安全嗎?
          • 業(yè)界爭論 Redlock,到底在爭論什么?哪種觀點(diǎn)是對的?
          • 分布式鎖到底用 Redis 還是 Zookeeper?
          • 實(shí)現(xiàn)一個有「容錯性」的分布式鎖,都需要考慮哪些問題?

          這篇文章,我就來把這些問題徹底講清楚。

          讀完這篇文章,你不僅可以徹底了解分布式鎖,還會對「分布式系統(tǒng)」有更加深刻的理解。

          文章有點(diǎn)長,但干貨很多,希望你可以耐心讀完。

          為什么需要分布式鎖?

          在開始講分布式鎖之前,有必要簡單介紹一下,為什么需要分布式鎖?

          與分布式鎖相對應(yīng)的是「單機(jī)鎖」,我們在寫多線程程序時,避免同時操作一個共享變量產(chǎn)生數(shù)據(jù)問題,通常會使用一把鎖來「互斥」,以保證共享變量的正確性,其使用范圍是在「同一個進(jìn)程」中。

          如果換做是多個進(jìn)程,需要同時操作一個共享資源,如何互斥呢?

          例如,現(xiàn)在的業(yè)務(wù)應(yīng)用通常都是微服務(wù)架構(gòu),這也意味著一個應(yīng)用會部署多個進(jìn)程,那這多個進(jìn)程如果需要修改 MySQL 中的同一行記錄時,為了避免操作亂序?qū)е聰?shù)據(jù)錯誤,此時,我們就需要引入「分布式鎖」來解決這個問題了。

          想要實(shí)現(xiàn)分布式鎖,必須借助一個外部系統(tǒng),所有進(jìn)程都去這個系統(tǒng)上申請「加鎖」。

          而這個外部系統(tǒng),必須要實(shí)現(xiàn)「互斥」的能力,即兩個請求同時進(jìn)來,只會給一個進(jìn)程返回成功,另一個返回失?。ɑ虻却?。

          這個外部系統(tǒng),可以是 MySQL,也可以是 Redis 或 Zookeeper。但為了追求更好的性能,我們通常會選擇使用 Redis 或 Zookeeper 來做。

          下面我就以 Redis 為主線,由淺入深,帶你深度剖析一下,分布式鎖的各種「安全性」問題,幫你徹底理解分布式鎖。

          分布式鎖怎么實(shí)現(xiàn)?

          我們從最簡單的開始講起。

          想要實(shí)現(xiàn)分布式鎖,必須要求 Redis 有「互斥」的能力,我們可以使用 SETNX 命令,這個命令表示SET if Not eXists,即如果 key 不存在,才會設(shè)置它的值,否則什么也不做。

          兩個客戶端進(jìn)程可以執(zhí)行這個命令,達(dá)到互斥,就可以實(shí)現(xiàn)一個分布式鎖。

          客戶端 1 申請加鎖,加鎖成功:

          127.0.0.1:6379> SETNX lock 1
          (integer) 1     // 客戶端1,加鎖成功

          客戶端 2 申請加鎖,因?yàn)樗蟮竭_(dá),加鎖失?。?/p>

          127.0.0.1:6379> SETNX lock 1
          (integer) 0     // 客戶端2,加鎖失敗

          此時,加鎖成功的客戶端,就可以去操作「共享資源」,例如,修改 MySQL 的某一行數(shù)據(jù),或者調(diào)用一個 API 請求。

          操作完成后,還要及時釋放鎖,給后來者讓出操作共享資源的機(jī)會。如何釋放鎖呢?

          也很簡單,直接使用 DEL 命令刪除這個 key 即可:

          127.0.0.1:6379> DEL lock // 釋放鎖
          (integer) 1

          這個邏輯非常簡單,整體的路程就是這樣:

          但是,它存在一個很大的問題,當(dāng)客戶端 1 拿到鎖后,如果發(fā)生下面的場景,就會造成「死鎖」:

          1. 程序處理業(yè)務(wù)邏輯異常,沒及時釋放鎖
          2. 進(jìn)程掛了,沒機(jī)會釋放鎖

          這時,這個客戶端就會一直占用這個鎖,而其它客戶端就「永遠(yuǎn)」拿不到這把鎖了。

          怎么解決這個問題呢?

          如何避免死鎖?

          我們很容易想到的方案是,在申請鎖時,給這把鎖設(shè)置一個「租期」。

          在 Redis 中實(shí)現(xiàn)時,就是給這個 key 設(shè)置一個「過期時間」。這里我們假設(shè),操作共享資源的時間不會超過 10s,那么在加鎖時,給這個 key 設(shè)置 10s 過期即可:

          127.0.0.1:6379> SETNX lock 1    // 加鎖
          (integer) 1
          127.0.0.1:6379> EXPIRE lock 10  // 10s后自動過期
          (integer) 1

          這樣一來,無論客戶端是否異常,這個鎖都可以在 10s 后被「自動釋放」,其它客戶端依舊可以拿到鎖。

          但這樣真的沒問題嗎?

          還是有問題。

          現(xiàn)在的操作,加鎖、設(shè)置過期是 2 條命令,有沒有可能只執(zhí)行了第一條,第二條卻「來不及」執(zhí)行的情況發(fā)生呢?例如:

          1. SETNX 執(zhí)行成功,執(zhí)行 EXPIRE 時由于網(wǎng)絡(luò)問題,執(zhí)行失敗
          2. SETNX 執(zhí)行成功,Redis 異常宕機(jī),EXPIRE 沒有機(jī)會執(zhí)行
          3. SETNX 執(zhí)行成功,客戶端異常崩潰,EXPIRE 也沒有機(jī)會執(zhí)行

          總之,這兩條命令不能保證是原子操作(一起成功),就有潛在的風(fēng)險導(dǎo)致過期時間設(shè)置失敗,依舊發(fā)生「死鎖」問題。

          怎么辦?

          在 Redis 2.6.12 版本之前,我們需要想盡辦法,保證 SETNX 和 EXPIRE 原子性執(zhí)行,還要考慮各種異常情況如何處理。

          但在 Redis 2.6.12 之后,Redis 擴(kuò)展了 SET 命令的參數(shù),用這一條命令就可以了:

          // 一條命令保證原子性執(zhí)行
          127.0.0.1:6379> SET lock 1 EX 10 NX
          OK

          這樣就解決了死鎖問題,也比較簡單。

          我們再來看分析下,它還有什么問題?

          試想這樣一種場景:

          1. 客戶端 1 加鎖成功,開始操作共享資源
          2. 客戶端 1 操作共享資源的時間,「超過」了鎖的過期時間,鎖被「自動釋放」
          3. 客戶端 2 加鎖成功,開始操作共享資源
          4. 客戶端 1 操作共享資源完成,釋放鎖(但釋放的是客戶端 2 的鎖)

          看到了么,這里存在兩個嚴(yán)重的問題:

          1. 鎖過期:客戶端 1 操作共享資源耗時太久,導(dǎo)致鎖被自動釋放,之后被客戶端 2 持有
          2. 釋放別人的鎖:客戶端 1 操作共享資源完成后,卻又釋放了客戶端 2 的鎖

          導(dǎo)致這兩個問題的原因是什么?我們一個個來看。

          第一個問題,可能是我們評估操作共享資源的時間不準(zhǔn)確導(dǎo)致的。

          例如,操作共享資源的時間「最慢」可能需要 15s,而我們卻只設(shè)置了 10s 過期,那這就存在鎖提前過期的風(fēng)險。

          過期時間太短,那增大冗余時間,例如設(shè)置過期時間為 20s,這樣總可以了吧?

          這樣確實(shí)可以「緩解」這個問題,降低出問題的概率,但依舊無法「徹底解決」問題。

          為什么?

          原因在于,客戶端在拿到鎖之后,在操作共享資源時,遇到的場景有可能是很復(fù)雜的,例如,程序內(nèi)部發(fā)生異常、網(wǎng)絡(luò)請求超時等等。

          既然是「預(yù)估」時間,也只能是大致計算,除非你能預(yù)料并覆蓋到所有導(dǎo)致耗時變長的場景,但這其實(shí)很難。

          有什么更好的解決方案嗎?

          別急,關(guān)于這個問題,我會在后面詳細(xì)來講對應(yīng)的解決方案。

          我們繼續(xù)來看第二個問題。

          第二個問題在于,一個客戶端釋放了其它客戶端持有的鎖。

          想一下,導(dǎo)致這個問題的關(guān)鍵點(diǎn)在哪?

          重點(diǎn)在于,每個客戶端在釋放鎖時,都是「無腦」操作,并沒有檢查這把鎖是否還「歸自己持有」,所以就會發(fā)生釋放別人鎖的風(fēng)險,這樣的解鎖流程,很不「嚴(yán)謹(jǐn)」!

          如何解決這個問題呢?

          鎖被別人釋放怎么辦?

          解決辦法是:客戶端在加鎖時,設(shè)置一個只有自己知道的「唯一標(biāo)識」進(jìn)去。

          例如,可以是自己的線程 ID,也可以是一個 UUID(隨機(jī)且唯一),這里我們以 UUID 舉例:

          // 鎖的VALUE設(shè)置為UUID
          127.0.0.1:6379> SET lock $uuid EX 20 NX
          OK

          這里假設(shè) 20s 操作共享時間完全足夠,先不考慮鎖自動過期的問題。

          之后,在釋放鎖時,要先判斷這把鎖是否還歸自己持有,偽代碼可以這么寫:

          // 鎖是自己的,才釋放
          if redis.get("lock") == $uuid:
              redis.del("lock")

          這里釋放鎖使用的是 GET + DEL 兩條命令,這時,又會遇到我們前面講的原子性問題了。

          1. 客戶端 1 執(zhí)行 GET,判斷鎖是自己的
          2. 客戶端 2 執(zhí)行了 SET 命令,強(qiáng)制獲取到鎖(雖然發(fā)生概率比較低,但我們需要嚴(yán)謹(jǐn)?shù)乜紤]鎖的安全性模型)
          3. 客戶端 1 執(zhí)行 DEL,卻釋放了客戶端 2 的鎖

          由此可見,這兩個命令還是必須要原子執(zhí)行才行。

          怎樣原子執(zhí)行呢?Lua 腳本。

          我們可以把這個邏輯,寫成 Lua 腳本,讓 Redis 來執(zhí)行。

          因?yàn)?Redis 處理每一個請求是「單線程」執(zhí)行的,在執(zhí)行一個 Lua 腳本時,其它請求必須等待,直到這個 Lua 腳本處理完成,這樣一來,GET + DEL 之間就不會插入其它命令了。

          安全釋放鎖的 Lua 腳本如下:

          // 判斷鎖是自己的,才釋放
          if redis.call("GET",KEYS[1]) == ARGV[1]
          then
              return redis.call("DEL",KEYS[1])
          else
              return 0
          end

          好了,這樣一路優(yōu)化,整個的加鎖、解鎖的流程就更「嚴(yán)謹(jǐn)」了。

          這里我們先小結(jié)一下,基于 Redis 實(shí)現(xiàn)的分布式鎖,一個嚴(yán)謹(jǐn)?shù)牡牧鞒倘缦拢?/p>

          1. 加鎖:SET lock_key $unique_id EX $expire_time NX
          2. 操作共享資源
          3. 釋放鎖:Lua 腳本,先 GET 判斷鎖是否歸屬自己,再 DEL 釋放鎖

          好,有了這個完整的鎖模型,讓我們重新回到前面提到的第一個問題。

          鎖過期時間不好評估怎么辦?

          鎖過期時間不好評估怎么辦?

          前面我們提到,鎖的過期時間如果評估不好,這個鎖就會有「提前」過期的風(fēng)險。

          當(dāng)時給的妥協(xié)方案是,盡量「冗余」過期時間,降低鎖提前過期的概率。

          這個方案其實(shí)也不能完美解決問題,那怎么辦呢?

          是否可以設(shè)計這樣的方案:加鎖時,先設(shè)置一個過期時間,然后我們開啟一個「守護(hù)線程」,定時去檢測這個鎖的失效時間,如果鎖快要過期了,操作共享資源還未完成,那么就自動對鎖進(jìn)行「續(xù)期」,重新設(shè)置過期時間。

          這確實(shí)一種比較好的方案。

          如果你是 Java 技術(shù)棧,幸運(yùn)的是,已經(jīng)有一個庫把這些工作都封裝好了:Redisson。

          Redisson 是一個 Java 語言實(shí)現(xiàn)的 Redis SDK 客戶端,在使用分布式鎖時,它就采用了「自動續(xù)期」的方案來避免鎖過期,這個守護(hù)線程我們一般也把它叫做「看門狗」線程。

          除此之外,這個 SDK 還封裝了很多易用的功能:

          • 可重入鎖
          • 樂觀鎖
          • 公平鎖
          • 讀寫鎖
          • Redlock(紅鎖,下面會詳細(xì)講)

          這個 SDK 提供的 API 非常友好,它可以像操作本地鎖的方式,操作分布式鎖。如果你是 Java 技術(shù)棧,可以直接把它用起來。

          這里不重點(diǎn)介紹 Redisson 的使用,大家可以看官方 Github 學(xué)習(xí)如何使用,比較簡單。

          到這里我們再小結(jié)一下,基于 Redis 的實(shí)現(xiàn)分布式鎖,前面遇到的問題,以及對應(yīng)的解決方案:

          • 死鎖:設(shè)置過期時間
          • 過期時間評估不好,鎖提前過期:守護(hù)線程,自動續(xù)期
          • 鎖被別人釋放:鎖寫入唯一標(biāo)識,釋放鎖先檢查標(biāo)識,再釋放

          還有哪些問題場景,會危害 Redis 鎖的安全性呢?

          之前分析的場景都是,鎖在「單個」Redis 實(shí)例中可能產(chǎn)生的問題,并沒有涉及到 Redis 的部署架構(gòu)細(xì)節(jié)。

          而我們在使用 Redis 時,一般會采用主從集群 + 哨兵的模式部署,這樣做的好處在于,當(dāng)主庫異常宕機(jī)時,哨兵可以實(shí)現(xiàn)「故障自動切換」,把從庫提升為主庫,繼續(xù)提供服務(wù),以此保證可用性。

          那當(dāng)「主從發(fā)生切換」時,這個分布鎖會依舊安全嗎?

          試想這樣的場景:

          1. 客戶端 1 在主庫上執(zhí)行 SET 命令,加鎖成功
          2. 此時,主庫異常宕機(jī),SET 命令還未同步到從庫上(主從復(fù)制是異步的)
          3. 從庫被哨兵提升為新主庫,這個鎖在新的主庫上,丟失了!

          可見,當(dāng)引入 Redis 副本后,分布鎖還是可能會受到影響。

          怎么解決這個問題?

          為此,Redis 的作者提出一種解決方案,就是我們經(jīng)常聽到的 Redlock(紅鎖)。

          它真的可以解決上面這個問題嗎?

          Redlock 真的安全嗎?

          好,終于到了這篇文章的重頭戲。???上面講的那么多問題,難道只是基礎(chǔ)?

          是的,那些只是開胃菜,真正的硬菜,從這里剛剛開始。

          如果上面講的內(nèi)容,你還沒有理解,我建議你重新閱讀一遍,先理清整個加鎖、解鎖的基本流程。

          如果你已經(jīng)對 Redlock 有所了解,這里可以跟著我再復(fù)習(xí)一遍,如果你不了解 Redlock,沒關(guān)系,我會帶你重新認(rèn)識它。

          值得提醒你的是,后面我不僅僅是講 Redlock 的原理,還會引出有關(guān)「分布式系統(tǒng)」中的很多問題,你最好跟緊我的思路,在腦中一起分析問題的答案。

          現(xiàn)在我們來看,Redis 作者提出的 Redlock 方案,是如何解決主從切換后,鎖失效問題的。

          Redlock 的方案基于 2 個前提:

          1. 不再需要部署從庫哨兵實(shí)例,只部署主庫
          2. 但主庫要部署多個,官方推薦至少 5 個實(shí)例

          也就是說,想用使用 Redlock,你至少要部署 5 個 Redis 實(shí)例,而且都是主庫,它們之間沒有任何關(guān)系,都是一個個孤立的實(shí)例。

          注意:不是部署 Redis Cluster,就是部署 5 個簡單的 Redis 實(shí)例。

          Redlock 具體如何使用呢?

          整體的流程是這樣的,一共分為 5 步:

          1. 客戶端先獲取「當(dāng)前時間戳T1」
          2. 客戶端依次向這 5 個 Redis 實(shí)例發(fā)起加鎖請求(用前面講到的 SET 命令),且每個請求會設(shè)置超時時間(毫秒級,要遠(yuǎn)小于鎖的有效時間),如果某一個實(shí)例加鎖失?。òňW(wǎng)絡(luò)超時、鎖被其它人持有等各種異常情況),就立即向下一個 Redis 實(shí)例申請加鎖
          3. 如果客戶端從 >=3 個(大多數(shù))以上 Redis 實(shí)例加鎖成功,則再次獲取「當(dāng)前時間戳T2」,如果 T2 - T1 < 鎖的過期時間,此時,認(rèn)為客戶端加鎖成功,否則認(rèn)為加鎖失敗
          4. 加鎖成功,去操作共享資源(例如修改 MySQL 某一行,或發(fā)起一個 API 請求)
          5. 加鎖失敗,向「全部節(jié)點(diǎn)」發(fā)起釋放鎖請求(前面講到的 Lua 腳本釋放鎖)

          我簡單幫你總結(jié)一下,有 4 個重點(diǎn):

          1. 客戶端在多個 Redis 實(shí)例上申請加鎖
          2. 必須保證大多數(shù)節(jié)點(diǎn)加鎖成功
          3. 大多數(shù)節(jié)點(diǎn)加鎖的總耗時,要小于鎖設(shè)置的過期時間
          4. 釋放鎖,要向全部節(jié)點(diǎn)發(fā)起釋放鎖請求

          第一次看可能不太容易理解,建議你把上面的文字多看幾遍,加深記憶。

          然后,記住這 5 步,非常重要,下面會根據(jù)這個流程,剖析各種可能導(dǎo)致鎖失效的問題假設(shè)。

          好,明白了 Redlock 的流程,我們來看 Redlock 為什么要這么做。

          1) 為什么要在多個實(shí)例上加鎖?

          本質(zhì)上是為了「容錯」,部分實(shí)例異常宕機(jī),剩余的實(shí)例加鎖成功,整個鎖服務(wù)依舊可用。

          2) 為什么大多數(shù)加鎖成功,才算成功?

          多個 Redis 實(shí)例一起來用,其實(shí)就組成了一個「分布式系統(tǒng)」。

          在分布式系統(tǒng)中,總會出現(xiàn)「異常節(jié)點(diǎn)」,所以,在談?wù)摲植际较到y(tǒng)問題時,需要考慮異常節(jié)點(diǎn)達(dá)到多少個,也依舊不會影響整個系統(tǒng)的「正確性」。

          這是一個分布式系統(tǒng)「容錯」問題,這個問題的結(jié)論是:如果只存在「故障」節(jié)點(diǎn),只要大多數(shù)節(jié)點(diǎn)正常,那么整個系統(tǒng)依舊是可以提供正確服務(wù)的。

          這個問題的模型,就是我們經(jīng)常聽到的「拜占庭將軍」問題,感興趣可以去看算法的推演過程。

          3) 為什么步驟 3 加鎖成功后,還要計算加鎖的累計耗時?

          因?yàn)椴僮鞯氖嵌鄠€節(jié)點(diǎn),所以耗時肯定會比操作單個實(shí)例耗時更久,而且,因?yàn)槭蔷W(wǎng)絡(luò)請求,網(wǎng)絡(luò)情況是復(fù)雜的,有可能存在延遲、丟包、超時等情況發(fā)生,網(wǎng)絡(luò)請求越多,異常發(fā)生的概率就越大。

          所以,即使大多數(shù)節(jié)點(diǎn)加鎖成功,但如果加鎖的累計耗時已經(jīng)「超過」了鎖的過期時間,那此時有些實(shí)例上的鎖可能已經(jīng)失效了,這個鎖就沒有意義了。

          4) 為什么釋放鎖,要操作所有節(jié)點(diǎn)?

          在某一個 Redis 節(jié)點(diǎn)加鎖時,可能因?yàn)椤妇W(wǎng)絡(luò)原因」導(dǎo)致加鎖失敗。

          例如,客戶端在一個 Redis 實(shí)例上加鎖成功,但在讀取響應(yīng)結(jié)果時,網(wǎng)絡(luò)問題導(dǎo)致讀取失敗,那這把鎖其實(shí)已經(jīng)在 Redis 上加鎖成功了。

          所以,釋放鎖時,不管之前有沒有加鎖成功,需要釋放「所有節(jié)點(diǎn)」的鎖,以保證清理節(jié)點(diǎn)上「殘留」的鎖。

          好了,明白了 Redlock 的流程和相關(guān)問題,看似 Redlock 確實(shí)解決了 Redis 節(jié)點(diǎn)異常宕機(jī)鎖失效的問題,保證了鎖的「安全性」。

          但事實(shí)真的如此嗎?

          Redlock 的爭論誰對誰錯?

          Redis 作者把這個方案一經(jīng)提出,就馬上受到業(yè)界著名的分布式系統(tǒng)專家的質(zhì)疑

          這個專家叫 Martin,是英國劍橋大學(xué)的一名分布式系統(tǒng)研究員。在此之前他曾是軟件工程師和企業(yè)家,從事大規(guī)模數(shù)據(jù)基礎(chǔ)設(shè)施相關(guān)的工作。它還經(jīng)常在大會做演講,寫博客,寫書,也是開源貢獻(xiàn)者。

          他馬上寫了篇文章,質(zhì)疑這個 Redlock 的算法模型是有問題的,并對分布式鎖的設(shè)計,提出了自己的看法。

          之后,Redis 作者 Antirez 面對質(zhì)疑,不甘示弱,也寫了一篇文章,反駁了對方的觀點(diǎn),并詳細(xì)剖析了 Redlock 算法模型的更多設(shè)計細(xì)節(jié)。

          而且,關(guān)于這個問題的爭論,在當(dāng)時互聯(lián)網(wǎng)上也引起了非常激烈的討論。

          二人思路清晰,論據(jù)充分,這是一場高手過招,也是分布式系統(tǒng)領(lǐng)域非常好的一次思想的碰撞!雙方都是分布式系統(tǒng)領(lǐng)域的專家,卻對同一個問題提出很多相反的論斷,究竟是怎么回事?

          下面我會從他們的爭論文章中,提取重要的觀點(diǎn),整理呈現(xiàn)給你。

          提醒:后面的信息量極大,可能不宜理解,最好放慢速度閱讀。

          分布式專家 Martin 對于 Relock 的質(zhì)疑

          在他的文章中,主要闡述了 4 個論點(diǎn):

          1) 分布式鎖的目的是什么?

          Martin 表示,你必須先清楚你在使用分布式鎖的目的是什么?

          他認(rèn)為有兩個目的。

          第一,效率。

          使用分布式鎖的互斥能力,是避免不必要地做同樣的兩次工作(例如一些昂貴的計算任務(wù))。如果鎖失效,并不會帶來「惡性」的后果,例如發(fā)了 2 次郵件等,無傷大雅。

          第二,正確性。

          使用鎖用來防止并發(fā)進(jìn)程互相干擾。如果鎖失效,會造成多個進(jìn)程同時操作同一條數(shù)據(jù),產(chǎn)生的后果是數(shù)據(jù)嚴(yán)重錯誤、永久性不一致、數(shù)據(jù)丟失等惡性問題,就像給患者服用了重復(fù)劑量的藥物,后果很嚴(yán)重。

          他認(rèn)為,如果你是為了前者——效率,那么使用單機(jī)版 Redis 就可以了,即使偶爾發(fā)生鎖失效(宕機(jī)、主從切換),都不會產(chǎn)生嚴(yán)重的后果。而使用 Redlock 太重了,沒必要。

          而如果是為了正確性,Martin 認(rèn)為 Redlock 根本達(dá)不到安全性的要求,也依舊存在鎖失效的問題!

          2) 鎖在分布式系統(tǒng)中會遇到的問題

          Martin 表示,一個分布式系統(tǒng),更像一個復(fù)雜的「野獸」,存在著你想不到的各種異常情況。

          這些異常場景主要包括三大塊,這也是分布式系統(tǒng)會遇到的三座大山:NPC。

          • N:Network Delay,網(wǎng)絡(luò)延遲
          • P:Process Pause,進(jìn)程暫停(GC)
          • C:Clock Drift,時鐘漂移

          Martin 用一個進(jìn)程暫停(GC)的例子,指出了 Redlock 安全性問題:

          1. 客戶端 1 請求鎖定節(jié)點(diǎn) A、B、C、D、E
          2. 客戶端 1 的拿到鎖后,進(jìn)入 GC(時間比較久)
          3. 所有 Redis 節(jié)點(diǎn)上的鎖都過期了
          4. 客戶端 2 獲取到了 A、B、C、D、E 上的鎖
          5. 客戶端 1 GC 結(jié)束,認(rèn)為成功獲取鎖
          6. 客戶端 2 也認(rèn)為獲取到了鎖,發(fā)生「沖突」

          Martin 認(rèn)為,GC 可能發(fā)生在程序的任意時刻,而且執(zhí)行時間是不可控的。

          注:當(dāng)然,即使是使用沒有 GC 的編程語言,在發(fā)生網(wǎng)絡(luò)延遲、時鐘漂移時,也都有可能導(dǎo)致 Redlock 出現(xiàn)問題,這里 Martin 只是拿 GC 舉例。

          3) 假設(shè)時鐘正確的是不合理的

          又或者,當(dāng)多個 Redis 節(jié)點(diǎn)「時鐘」發(fā)生問題時,也會導(dǎo)致 Redlock 鎖失效。

          1. 客戶端 1 獲取節(jié)點(diǎn) A、B、C 上的鎖,但由于網(wǎng)絡(luò)問題,無法訪問 D 和 E
          2. 節(jié)點(diǎn) C 上的時鐘「向前跳躍」,導(dǎo)致鎖到期
          3. 客戶端 2 獲取節(jié)點(diǎn) C、D、E 上的鎖,由于網(wǎng)絡(luò)問題,無法訪問 A 和 B
          4. 客戶端 1 和 2 現(xiàn)在都相信它們持有了鎖(沖突)

          Martin 覺得,Redlock 必須「強(qiáng)依賴」多個節(jié)點(diǎn)的時鐘是保持同步的,一旦有節(jié)點(diǎn)時鐘發(fā)生錯誤,那這個算法模型就失效了。

          即使 C 不是時鐘跳躍,而是「崩潰后立即重啟」,也會發(fā)生類似的問題。

          Martin 繼續(xù)闡述,機(jī)器的時鐘發(fā)生錯誤,是很有可能發(fā)生的:

          • 系統(tǒng)管理員「手動修改」了機(jī)器時鐘
          • 機(jī)器時鐘在同步 NTP 時間時,發(fā)生了大的「跳躍」

          總之,Martin 認(rèn)為,Redlock 的算法是建立在「同步模型」基礎(chǔ)上的,有大量資料研究表明,同步模型的假設(shè),在分布式系統(tǒng)中是有問題的。

          在混亂的分布式系統(tǒng)的中,你不能假設(shè)系統(tǒng)時鐘就是對的,所以,你必須非常小心你的假設(shè)。

          4) 提出 fecing token 的方案,保證正確性

          相對應(yīng)的,Martin 提出一種被叫作 fecing token 的方案,保證分布式鎖的正確性。

          這個模型流程如下:

          1. 客戶端在獲取鎖時,鎖服務(wù)可以提供一個「遞增」的 token
          2. 客戶端拿著這個 token 去操作共享資源
          3. 共享資源可以根據(jù) token 拒絕「后來者」的請求

          這樣一來,無論 NPC 哪種異常情況發(fā)生,都可以保證分布式鎖的安全性,因?yàn)樗墙⒃凇府惒侥P汀股系摹?/p>

          而 Redlock 無法提供類似 fecing token 的方案,所以它無法保證安全性。

          他還表示,一個好的分布式鎖,無論 NPC 怎么發(fā)生,可以不在規(guī)定時間內(nèi)給出結(jié)果,但并不會給出一個錯誤的結(jié)果。也就是只會影響到鎖的「性能」(或稱之為活性),而不會影響它的「正確性」。

          Martin 的結(jié)論:

          1、Redlock 不倫不類:它對于效率來講,Redlock 比較重,沒必要這么做,而對于正確性來說,Redlock 是不夠安全的。

          2、時鐘假設(shè)不合理:該算法對系統(tǒng)時鐘做出了危險的假設(shè)(假設(shè)多個節(jié)點(diǎn)機(jī)器時鐘都是一致的),如果不滿足這些假設(shè),鎖就會失效。

          3、無法保證正確性:Redlock 不能提供類似 fencing token 的方案,所以解決不了正確性的問題。為了正確性,請使用有「共識系統(tǒng)」的軟件,例如 Zookeeper。

          好了,以上就是 Martin 反對使用 Redlock 的觀點(diǎn),看起來有理有據(jù)。

          下面我們來看 Redis 作者 Antirez 是如何反駁的。

          Redis 作者 Antirez 的反駁

          在 Redis 作者的文章中,重點(diǎn)有 3 個:

          1) 解釋時鐘問題

          首先,Redis 作者一眼就看穿了對方提出的最為核心的問題:時鐘問題。

          Redis 作者表示,Redlock 并不需要完全一致的時鐘,只需要大體一致就可以了,允許有「誤差」。

          例如要計時 5s,但實(shí)際可能記了 4.5s,之后又記了 5.5s,有一定誤差,但只要不超過「誤差范圍」鎖失效時間即可,這種對于時鐘的精度要求并不是很高,而且這也符合現(xiàn)實(shí)環(huán)境。

          對于對方提到的「時鐘修改」問題,Redis 作者反駁到:

          1. 手動修改時鐘:不要這么做就好了,否則你直接修改 Raft 日志,那 Raft 也會無法工作...
          2. 時鐘跳躍:通過「恰當(dāng)?shù)倪\(yùn)維」,保證機(jī)器時鐘不會大幅度跳躍(每次通過微小的調(diào)整來完成),實(shí)際上這是可以做到的

          為什么 Redis 作者優(yōu)先解釋時鐘問題?因?yàn)樵诤竺娴姆瘩g過程中,需要依賴這個基礎(chǔ)做進(jìn)一步解釋。

          2) 解釋網(wǎng)絡(luò)延遲、GC 問題

          之后,Redis 作者對于對方提出的,網(wǎng)絡(luò)延遲、進(jìn)程 GC 可能導(dǎo)致 Redlock 失效的問題,也做了反駁:

          我們重新回顧一下,Martin 提出的問題假設(shè):

          1. 客戶端 1 請求鎖定節(jié)點(diǎn) A、B、C、D、E
          2. 客戶端 1 的拿到鎖后,進(jìn)入 GC
          3. 所有 Redis 節(jié)點(diǎn)上的鎖都過期了
          4. 客戶端 2 獲取節(jié)點(diǎn) A、B、C、D、E 上的鎖
          5. 客戶端 1 GC 結(jié)束,認(rèn)為成功獲取鎖
          6. 客戶端 2 也認(rèn)為獲取到鎖,發(fā)生「沖突」

          Redis 作者反駁到,這個假設(shè)其實(shí)是有問題的,Redlock 是可以保證鎖安全的。

          這是怎么回事呢?

          還記得前面介紹 Redlock 流程的那 5 步嗎?這里我再拿過來讓你復(fù)習(xí)一下。

          1. 客戶端先獲取「當(dāng)前時間戳T1」
          2. 客戶端依次向這 5 個 Redis 實(shí)例發(fā)起加鎖請求(用前面講到的 SET 命令),且每個請求會設(shè)置超時時間(毫秒級,要遠(yuǎn)小于鎖的有效時間),如果某一個實(shí)例加鎖失敗(包括網(wǎng)絡(luò)超時、鎖被其它人持有等各種異常情況),就立即向下一個 Redis 實(shí)例申請加鎖
          3. 如果客戶端從 3 個(大多數(shù))以上 Redis 實(shí)例加鎖成功,則再次獲取「當(dāng)前時間戳T2」,如果 T2 - T1 < 鎖的過期時間,此時,認(rèn)為客戶端加鎖成功,否則認(rèn)為加鎖失敗
          4. 加鎖成功,去操作共享資源(例如修改 MySQL 某一行,或發(fā)起一個 API 請求)
          5. 加鎖失敗,向「全部節(jié)點(diǎn)」發(fā)起釋放鎖請求(前面講到的 Lua 腳本釋放鎖)

          注意,重點(diǎn)是 1-3,在步驟 3,加鎖成功后為什么要重新獲取「當(dāng)前時間戳T2」?還用 T2 - T1 的時間,與鎖的過期時間做比較?

          Redis 作者強(qiáng)調(diào):如果在 1-3 發(fā)生了網(wǎng)絡(luò)延遲、進(jìn)程 GC 等耗時長的異常情況,那在第 3 步 T2 - T1,是可以檢測出來的,如果超出了鎖設(shè)置的過期時間,那這時就認(rèn)為加鎖會失敗,之后釋放所有節(jié)點(diǎn)的鎖就好了!

          Redis 作者繼續(xù)論述,如果對方認(rèn)為,發(fā)生網(wǎng)絡(luò)延遲、進(jìn)程 GC 是在步驟 3 之后,也就是客戶端確認(rèn)拿到了鎖,去操作共享資源的途中發(fā)生了問題,導(dǎo)致鎖失效,那這不止是 Redlock 的問題,任何其它鎖服務(wù)例如 Zookeeper,都有類似的問題,這不在討論范疇內(nèi)。

          這里我舉個例子解釋一下這個問題:

          1. 客戶端通過 Redlock 成功獲取到鎖(通過了大多數(shù)節(jié)點(diǎn)加鎖成功、加鎖耗時檢查邏輯)
          2. 客戶端開始操作共享資源,此時發(fā)生網(wǎng)絡(luò)延遲、進(jìn)程 GC 等耗時很長的情況
          3. 此時,鎖過期自動釋放
          4. 客戶端開始操作 MySQL(此時的鎖可能會被別人拿到,鎖失效)

          Redis 作者這里的結(jié)論就是:

          • 客戶端在拿到鎖之前,無論經(jīng)歷什么耗時長問題,Redlock 都能夠在第 3 步檢測出來
          • 客戶端在拿到鎖之后,發(fā)生 NPC,那 Redlock、Zookeeper 都無能為力

          所以,Redis 作者認(rèn)為 Redlock 在保證時鐘正確的基礎(chǔ)上,是可以保證正確性的。

          3) 質(zhì)疑 fencing token 機(jī)制

          Redis 作者對于對方提出的 fecing token 機(jī)制,也提出了質(zhì)疑,主要分為 2 個問題,這里最不宜理解,請跟緊我的思路。

          第一,這個方案必須要求要操作的「共享資源服務(wù)器」有拒絕「舊 token」的能力。

          例如,要操作 MySQL,從鎖服務(wù)拿到一個遞增數(shù)字的 token,然后客戶端要帶著這個 token 去改 MySQL 的某一行,這就需要利用 MySQL 的「事物隔離性」來做。

          // 兩個客戶端必須利用事物和隔離性達(dá)到目的
          // 注意 token 的判斷條件
          UPDATE table T SET val = $new_val WHERE id = $id AND current_token < $token

          但如果操作的不是 MySQL 呢?例如向磁盤上寫一個文件,或發(fā)起一個 HTTP 請求,那這個方案就無能為力了,這對要操作的資源服務(wù)器,提出了更高的要求。

          也就是說,大部分要操作的資源服務(wù)器,都是沒有這種互斥能力的。

          再者,既然資源服務(wù)器都有了「互斥」能力,那還要分布式鎖干什么?

          所以,Redis 作者認(rèn)為這個方案是站不住腳的。

          第二,退一步講,即使 Redlock 沒有提供 fecing token 的能力,但 Redlock 已經(jīng)提供了隨機(jī)值(就是前面講的 UUID),利用這個隨機(jī)值,也可以達(dá)到與 fecing token 同樣的效果。

          如何做呢?

          Redis 作者只是提到了可以完成 fecing token 類似的功能,但卻沒有展開相關(guān)細(xì)節(jié),根據(jù)我查閱的資料,大概流程應(yīng)該如下,如有錯誤,歡迎交流~

          1. 客戶端使用 Redlock 拿到鎖
          2. 客戶端在操作共享資源之前,先把這個鎖的 VALUE,在要操作的共享資源上做標(biāo)記
          3. 客戶端處理業(yè)務(wù)邏輯,最后,在修改共享資源時,判斷這個標(biāo)記是否與之前一樣,一樣才修改(類似 CAS 的思路)

          還是以 MySQL 為例,舉個例子就是這樣的:

          1. 客戶端使用 Redlock 拿到鎖
          2. 客戶端要修改 MySQL 表中的某一行數(shù)據(jù)之前,先把鎖的 VALUE 更新到這一行的某個字段中(這里假設(shè)為 current_token 字段)
          3. 客戶端處理業(yè)務(wù)邏輯
          4. 客戶端修改 MySQL 的這一行數(shù)據(jù),把 VALUE 當(dāng)做 WHERE 條件,再修改
          UPDATE table T SET val = $new_val WHERE id = $id AND current_token = $redlock_value

          可見,這種方案依賴 MySQL 的事物機(jī)制,也達(dá)到對方提到的 fecing token 一樣的效果。

          但這里還有個小問題,是網(wǎng)友參與問題討論時提出的:兩個客戶端通過這種方案,先「標(biāo)記」再「檢查+修改」共享資源,那這兩個客戶端的操作順序無法保證???

          而用 Martin 提到的 fecing token,因?yàn)檫@個 token 是單調(diào)遞增的數(shù)字,資源服務(wù)器可以拒絕小的 token 請求,保證了操作的「順序性」!

          Redis 作者對這問題做了不同的解釋,我覺得很有道理,他解釋道:分布式鎖的本質(zhì),是為了「互斥」,只要能保證兩個客戶端在并發(fā)時,一個成功,一個失敗就好了,不需要關(guān)心「順序性」。

          前面 Martin 的質(zhì)疑中,一直很關(guān)心這個順序性問題,但 Redis 的作者的看法卻不同。

          綜上,Redis 作者的結(jié)論:

          1、作者同意對方關(guān)于「時鐘跳躍」對 Redlock 的影響,但認(rèn)為時鐘跳躍是可以避免的,取決于基礎(chǔ)設(shè)施和運(yùn)維。

          2、Redlock 在設(shè)計時,充分考慮了 NPC 問題,在 Redlock 步驟 3 之前出現(xiàn) NPC,可以保證鎖的正確性,但在步驟 3 之后發(fā)生 NPC,不止是 Redlock 有問題,其它分布式鎖服務(wù)同樣也有問題,所以不在討論范疇內(nèi)。

          是不是覺得很有意思?

          在分布式系統(tǒng)中,一個小小的鎖,居然可能會遇到這么多問題場景,影響它的安全性!

          不知道你看完雙方的觀點(diǎn),更贊同哪一方的說法呢?

          別急,后面我還會綜合以上論點(diǎn),談?wù)勛约旱睦斫狻?/p>

          好,講完了雙方對于 Redis 分布鎖的爭論,你可能也注意到了,Martin 在他的文章中,推薦使用 Zookeeper 實(shí)現(xiàn)分布式鎖,認(rèn)為它更安全,確實(shí)如此嗎?

          基于 Zookeeper 的鎖安全嗎?

          如果你有了解過 Zookeeper,基于它實(shí)現(xiàn)的分布式鎖是這樣的:

          1. 客戶端 1 和 2 都嘗試創(chuàng)建「臨時節(jié)點(diǎn)」,例如 /lock
          2. 假設(shè)客戶端 1 先到達(dá),則加鎖成功,客戶端 2 加鎖失敗
          3. 客戶端 1 操作共享資源
          4. 客戶端 1 刪除 /lock 節(jié)點(diǎn),釋放鎖

          你應(yīng)該也看到了,Zookeeper 不像 Redis 那樣,需要考慮鎖的過期時間問題,它是采用了「臨時節(jié)點(diǎn)」,保證客戶端 1 拿到鎖后,只要連接不斷,就可以一直持有鎖。

          而且,如果客戶端 1 異常崩潰了,那么這個臨時節(jié)點(diǎn)會自動刪除,保證了鎖一定會被釋放。

          不錯,沒有鎖過期的煩惱,還能在異常時自動釋放鎖,是不是覺得很完美?

          其實(shí)不然。

          思考一下,客戶端 1 創(chuàng)建臨時節(jié)點(diǎn)后,Zookeeper 是如何保證讓這個客戶端一直持有鎖呢?

          原因就在于,客戶端 1 此時會與 Zookeeper 服務(wù)器維護(hù)一個 Session,這個 Session 會依賴客戶端「定時心跳」來維持連接。

          如果 Zookeeper 長時間收不到客戶端的心跳,就認(rèn)為這個 Session 過期了,也會把這個臨時節(jié)點(diǎn)刪除。

          同樣地,基于此問題,我們也討論一下 GC 問題對 Zookeeper 的鎖有何影響:

          1. 客戶端 1 創(chuàng)建臨時節(jié)點(diǎn) /lock 成功,拿到了鎖
          2. 客戶端 1 發(fā)生長時間 GC
          3. 客戶端 1 無法給 Zookeeper 發(fā)送心跳,Zookeeper 把臨時節(jié)點(diǎn)「刪除」
          4. 客戶端 2 創(chuàng)建臨時節(jié)點(diǎn) /lock 成功,拿到了鎖
          5. 客戶端 1 GC 結(jié)束,它仍然認(rèn)為自己持有鎖(沖突)

          可見,即使是使用 Zookeeper,也無法保證進(jìn)程 GC、網(wǎng)絡(luò)延遲異常場景下的安全性。

          這就是前面 Redis 作者在反駁的文章中提到的:如果客戶端已經(jīng)拿到了鎖,但客戶端與鎖服務(wù)器發(fā)生「失聯(lián)」(例如 GC),那不止 Redlock 有問題,其它鎖服務(wù)都有類似的問題,Zookeeper 也是一樣!

          所以,這里我們就能得出結(jié)論了:一個分布式鎖,在極端情況下,不一定是安全的。

          如果你的業(yè)務(wù)數(shù)據(jù)非常敏感,在使用分布式鎖時,一定要注意這個問題,不能假設(shè)分布式鎖 100% 安全。

          好,現(xiàn)在我們來總結(jié)一下 Zookeeper 在使用分布式鎖時優(yōu)劣:

          Zookeeper 的優(yōu)點(diǎn):

          1. 不需要考慮鎖的過期時間
          2. watch 機(jī)制,加鎖失敗,可以 watch 等待鎖釋放,實(shí)現(xiàn)樂觀鎖

          但它的劣勢是:

          1. 性能不如 Redis
          2. 部署和運(yùn)維成本高
          3. 客戶端與 Zookeeper 的長時間失聯(lián),鎖被釋放問題

          我對分布式鎖的理解

          好了,前面詳細(xì)介紹了基于 Redis 的 Redlock 和 Zookeeper 實(shí)現(xiàn)的分布鎖,在各種異常情況下的安全性問題,下面我想和你聊一聊我的看法,僅供參考,不喜勿噴。

          1) 到底要不要用 Redlock?

          前面也分析了,Redlock 只有建立在「時鐘正確」的前提下,才能正常工作,如果你可以保證這個前提,那么可以拿來使用。

          但保證時鐘正確,我認(rèn)為并不是你想的那么簡單就能做到的。

          第一,從硬件角度來說,時鐘發(fā)生偏移是時有發(fā)生,無法避免。

          例如,CPU 溫度、機(jī)器負(fù)載、芯片材料都是有可能導(dǎo)致時鐘發(fā)生偏移的。

          第二,從我的工作經(jīng)歷來說,曾經(jīng)就遇到過時鐘錯誤、運(yùn)維暴力修改時鐘的情況發(fā)生,進(jìn)而影響了系統(tǒng)的正確性,所以,人為錯誤也是很難完全避免的。

          所以,我對 Redlock 的個人看法是,盡量不用它,而且它的性能不如單機(jī)版 Redis,部署成本也高,我還是會優(yōu)先考慮使用主從+ 哨兵的模式 實(shí)現(xiàn)分布式鎖。

          那正確性如何保證呢?第二點(diǎn)給你答案。

          2) 如何正確使用分布式鎖?

          在分析 Martin 觀點(diǎn)時,它提到了 fecing token 的方案,給我了很大的啟發(fā),雖然這種方案有很大的局限性,但對于保證「正確性」的場景,是一個非常好的思路。

          所以,我們可以把這兩者結(jié)合起來用:

          1、使用分布式鎖,在上層完成「互斥」目的,雖然極端情況下鎖會失效,但它可以最大程度把并發(fā)請求阻擋在最上層,減輕操作資源層的壓力。

          2、但對于要求數(shù)據(jù)絕對正確的業(yè)務(wù),在資源層一定要做好「兜底」,設(shè)計思路可以借鑒 fecing token 的方案來做。

          兩種思路結(jié)合,我認(rèn)為對于大多數(shù)業(yè)務(wù)場景,已經(jīng)可以滿足要求了。

          總結(jié)

          好了,總結(jié)一下。

          這篇文章,我們主要探討了基于 Redis 實(shí)現(xiàn)的分布式鎖,究竟是否安全這個問題。

          從最簡單分布式鎖的實(shí)現(xiàn),到處理各種異常場景,再到引出 Redlock,以及兩個分布式專家的辯論,得出了 Redlock 的適用場景。

          最后,我們還對比了 Zookeeper 在做分布式鎖時,可能會遇到的問題,以及與 Redis 的差異。

          這里我把這些內(nèi)容總結(jié)成了思維導(dǎo)圖,方便你理解。

          瀏覽 61
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久热精品视频在线播放 | av片在线观看 | 国产乱伦毛片 | 婷婷亚洲综合_久久精品男人 | 久久在线视频 |