<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 分布式鎖

          共 8185字,需瀏覽 17分鐘

           ·

          2023-05-28 13:46

          0. 前言

          Redis 是日常開發(fā)中經(jīng)常使用到的中間件,以優(yōu)秀的性能著稱。但是 Redis 分布式鎖可以說是飽受爭議,很多人認為 Redis 并不適合作為分布式鎖。它確實存在著一些問題,今天我準備聊一聊 Redis 分布式鎖如何實現(xiàn)、有什么問題、該如何解決以及它的進階版本紅鎖(Red Lock)解決了哪些問題,又帶來了哪些新的問題

          1. Redis 分布式鎖的標準實現(xiàn)方式

          我們以一個 Redis 單實例為例

          1.1. 上鎖原理

          Redis 通過 SET key value NX 命令實現(xiàn)鎖的互斥機制。SETNX 含義為(「SET」 if 「N」ot e「X」ists),只有在 key 不存在時,才能 SET 成功

          完整的上鎖命令如下所示

                #?NX?代表?key?不存在才能?SET?成功,PX?指定?key?的過期時間
          SET?resource_name?client_id?NX?PX?30000

          key 的過期時間,就是鎖的持有時間(或者說釋放時間)

          client_id 可以是任何隨機的唯一值(例如 UUID),它存在的意義是用于解鎖。只有 client_id 匹配時,才能解鎖(保證只有持有鎖的客戶端才可以解鎖,防止其他客戶端錯誤的解鎖)

          1.2. 解鎖原理

          解鎖本質(zhì)上就是刪除 key,或者 key 過期。

          主動解鎖通常使用 lua 腳本進行,因為我們希望原子性的完成判斷和刪除邏輯

                if?redis.call("get",KEYS[1])?==?ARGV[1]?then
          ????return?redis.call("del",KEYS[1])
          else
          ????return?0
          end
          2. Redis 分布式鎖的問題

          通過上一章節(jié),我們已經(jīng)了解到了,如何利用 Redis 機制實現(xiàn)一個分布式鎖,現(xiàn)在我們要更深入的討論下 Redis 是否可靠

          2.1. Redis 鎖未設置過期時間,導致死鎖

          場景:我使用 SET key client_id NX 持有了一個鎖,但是我并沒有設置過期時間。此時我的應用進程突然掛了,并沒有來得及進行解鎖操作。此時任何客戶端都無法再持有這個鎖了,必須得人工介入刪除掉這個 key,業(yè)務才能繼續(xù)流轉

          對于這個場景有什么好的解決辦法呢

          Redission 提供了一個很好的解決方案。Redission 提供了一個“看門狗”機制

          如果用戶在申請鎖的時候沒有指定鎖的釋放時間,此時 Redission 會為鎖指定一個 30 秒的過期時間。在鎖釋放之前,再額外使用一個線程不斷地延長 key 的過期時間。這樣就能保證即使應用進程意外掛掉,也不會出現(xiàn)“死鎖”

          Redission 使用了 Netty API 來實現(xiàn)“看門狗”,這里為了方便大家理解,我使用 Java API 做一個簡單的示例以供參考

                public?class?WatchDogExample?{
          ????/**
          ?????*?定義線程池
          ?????*/

          ????static?ScheduledExecutorService?scheduled?=?new?ScheduledThreadPoolExecutor(8,?new?ThreadFactory()?{
          ????????private?final?AtomicInteger?threadNumber?=?new?AtomicInteger(1);

          ????????@Override
          ????????public?Thread?newThread(Runnable?r)?{
          ????????????Thread?thread?=?new?Thread(r,?"watchDog-"?+?threadNumber.getAndIncrement());
          ????????????//?設置為守護線程,防止主線程意外掛掉,看門狗線程依舊在執(zhí)行
          ????????????thread.setDaemon(false);
          ????????????return?thread;
          ????????}
          ????});

          ????public?static?void?main(String[]?args)?{
          ????????//?模擬獲得鎖?tryLock()
          ????????//?SET?NX?命令,過期時間為?30s
          ????????System.out.println("SET?KEY?VALUE?NX?EX?30");

          ????????//?向線程池提交?KEY?續(xù)期任務
          ????????ScheduledFuture<?>?future?=?scheduled.scheduleAtFixedRate(()?->?{
          ????????????if?(Thread.currentThread().isInterrupted())?{
          ????????????????return;
          ????????????}
          ????????????//?EXPIRE?命令續(xù)期key?30s
          ????????????System.out.println("EXPIRE?KEY?30");
          ????????},?10,?10,?TimeUnit.SECONDS);


          ????????try?{
          ????????????//?模擬業(yè)務流程
          ????????????Thread.sleep(20000);
          ????????}?catch?(InterruptedException?e)?{
          ????????}

          ????????//?解鎖 unlock。停止任務調(diào)度?&?刪除key
          ????????future.cancel(true);
          ????????System.out.println("DEL?KEY");

          ????????try?{
          ????????????//?保證 main 線程不停止。如果 main 線程停止,守護線程將結束
          ????????????new?CountDownLatch(1).await();
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????}

          }

          2.2. Redis 鎖過期了,但是應用進程還在操作共享資源

          這種情況,我的理解是,如果你明確指定了鎖的過期時間,那么你就必須保證在這個時間內(nèi)完成對共享資源的操作。如果你無法保證,就使用 2.1 的方式,不指定過期時間

          2.3. 如何實現(xiàn)鎖的等待

          其實 SETNX 實現(xiàn)的效果僅是一個 tryLock,以 Java 中常用的鎖作為參考。通常來說我們?nèi)绻麩o法立即獲得鎖,是可以選擇等待鎖釋放后,繼續(xù)嘗試獲取鎖。那么 Redis 分布式鎖可以實現(xiàn)這一效果嗎?

          當然可以了,參考 Redission 的實現(xiàn),我們可以利用 Redis 的發(fā)布訂閱來實現(xiàn)鎖的等待,思路如下

          a. 嘗試獲取鎖

          b. 獲取鎖失敗,訂閱鎖釋放消息,線程進入等待狀態(tài)

          c. 其它客戶端解鎖,發(fā)布鎖釋放消息

          d. 接受到鎖釋放消息,回到步驟 a

          感興趣的同學,可以自行閱讀下 Redission 源碼

          44dfb4bfc0d5f5507008fb1c44368505.webp圖片來源小米技術團隊

          2.4. Redis 鎖是不可重入鎖?

          首先我們要明白,可重入鎖有什么意義?

          1. Java synchronized、ReentrantLock 都是可重入鎖。可重入鎖,即同一個線程可以多次獲取同一個鎖,其意義在于防止代碼出現(xiàn)死鎖。

          假設我們的 Redis 鎖已經(jīng)實現(xiàn)了 2.3 中提到的鎖等待功能,此時我設置了鎖的最大等待時間為 -1(無限等待),鎖的持有時間也是 -1。我在編碼的時候,沒有注意,多次獲取了相同的鎖。由于是無限等待,我的代碼在第二次獲取鎖時,就會出現(xiàn)死鎖,永遠的卡在那里。

          1. 對于 Redis 鎖來說,可重入也可以代表鎖續(xù)期

          在理解了可重入鎖的意義之后,我們該如何讓 Redis 分布式鎖支持重入呢?同樣參考 Redission 即可

          Redission 使用 Redis Hash 結構,存儲結構為 lock_key,client_id,重入次數(shù)

                ????<T>?RFuture<T>?tryLockInnerAsync(long?leaseTime,?TimeUnit?unit,?long?threadId,?RedisStrictCommand<T>?command)?{
          ????????internalLockLeaseTime?=?unit.toMillis(leaseTime);

          ????????return?commandExecutor.evalWriteAsync(getName(),?LongCodec.INSTANCE,?command,
          ??????????????????//?是否存在鎖
          ??????????????????"if?(redis.call('exists',?KEYS[1])?==?0)?then?"?+
          ???????????????????????//?不存在則創(chuàng)建
          ??????????????????????"redis.call('hset',?KEYS[1],?ARGV[2],?1);?"?+
          ??????????????????????//?設置過期時間
          ??????????????????????"redis.call('pexpire',?KEYS[1],?ARGV[1]);?"?+
          ??????????????????????//?競爭鎖成功?返回null
          ??????????????????????"return?nil;?"?+
          ??????????????????"end;?"?+
          ???????????????????//?如果鎖已經(jīng)被當前線程獲取
          ??????????????????"if?(redis.call('hexists',?KEYS[1],?ARGV[2])?==?1)?then?"?+
          ???????????????????????//?重入次數(shù)加1
          ??????????????????????"redis.call('hincrby',?KEYS[1],?ARGV[2],?1);?"?+
          ??????????????????????"redis.call('pexpire',?KEYS[1],?ARGV[1]);?"?+
          ??????????????????????"return?nil;?"?+
          ??????????????????"end;?"?+
          ??????????????????//?鎖被其他線程獲取,返回鎖的過期時間
          ??????????????????"return?redis.call('pttl',?KEYS[1]);",

          ????????????????????//?下面三個參數(shù)分別為?KEYS[1],?ARGV[1],?ARGV[2]
          ????????????????????//?即鎖的name,鎖釋放時間,當前線程唯一標識
          ????????????????????Collections.<Object>singletonList(getName()),?internalLockLeaseTime,?getLockName(threadId));
          ????}

          2.5. Redis 主備切換,導致多個客戶端同時持有鎖

          這個是 Redis 作為分布式鎖最大的問題。

          通常來說,我們會通過使用 Redis Sentinel 或者 Redis Cluster 來提高 Redis 的可用性,但是在提高 Redis 可用性的同時,也帶來了丟失數(shù)據(jù)的風險

          場景:現(xiàn)在我們有一個 Redis Sentinel,客戶端 A 從 Redis Master 獲取了鎖(寫入了一個 key),此時 Master 掛了。并且非常倒霉,在掛掉之前并沒有將客戶端 A 寫入的數(shù)據(jù)同步到 Slave 節(jié)點,然后 Slave 升級為 Master,客戶端 B 也獲取了相同的鎖。

          這就是 Redis 分布式鎖最尷尬的地方,當提高了 Redis 可用性之后,居然無法保證鎖的互斥性,這是讓人難以接受的。

          關于這個問題,有什么好的解決方法呢?就是我們接下來要講到的 Redis RedLock

          3. Redis RedLock

          3.1 RedLock 核心邏輯

          該算法的前提是我們需要準備 N 個完全獨立的 Redis Master。我們參考 Redis 官方文檔,將 N 設為 5

          1. 獲取當前時間戳(毫秒)

          2. 嘗試對 N 個實例進行 lock 操作,在所有實例中使用相同的 key 和 value。在執(zhí)行 Redis 命令時,需要設置一個較小的 command timeout。例如鎖的施放時間是 10s,則 command timeout 范圍最好在 5 ~ 50ms。因為我們要與多個 Redis 實例通信,盡可能防止命令阻塞在某臺實例中。

          3. 當客戶端能夠在大多數(shù)實例中獲取鎖(大多數(shù)至少為 N / 2 + 1),并且獲取鎖的總用時小于鎖的釋放時間,則認為成功持有鎖

          4. 鎖的實際持有時間 = 執(zhí)行的持有時間(或者說施放時間)- 獲取鎖用時

          5. 如果客戶端未能成功持有鎖(不能滿足步驟3),則對所有已經(jīng)加鎖的實例進行 unlock 操作

          針對每一個節(jié)點的 lock 和 unlock 操作與上邊提到過的實現(xiàn)方式相同

          3.2 RedLock 潛在問題

          在網(wǎng)上看到了國外的分布式專家與 RedLock 作者對 RedLock 是否可靠進行了許多的討論。

          再結合一些其他的文章,對 RedLock 的問題總結一下,討論中認為 RedLock 不可靠的幾個關鍵點

          3.2.1. 時間鐘躍進

          Redis key 的過期時間,最終會計算出一個時間戳。Redis 保證在系統(tǒng)時間超過這個時間戳時,刪除這個 key。

          如果對系統(tǒng)時間進行修改,例如快進了一段時間,可能會造成 key 失效,最終多個客戶端同時獲得鎖

          解決方法:

          1. 合理運維,不要修改系統(tǒng)時間。通常來說也沒有人會這么做

          3.2.2. 客戶端進程 GC 時間過長帶來的問題

          客戶端進程 GC 時間過長,導致鎖過期,但是客戶端無法感知,最終可能導致多個客戶端同時持有鎖。

          上文提到了使用看門狗的概念,一定程度上可以解決了鎖過期導致多個客戶端同時持有鎖的問題。但是如果真的 GC 時間真的特別長,導致看門狗機制也無法續(xù)期 key 的話,那確實會讓多個客戶端都持有鎖。

          解決方法:

          1. 延長 watchDog 的過期時間
          2. 優(yōu)化 GC 時間

          3.2.3 網(wǎng)絡分區(qū)帶來的問題

          現(xiàn)有 A、B、C、D、E 5臺 Redis 實例,分別部署在五臺機器上。客戶端如果只能訪問到 A、B 兩臺,與 C、D、E 網(wǎng)絡無法通信。這種情況和 Redis 宕機一樣,在網(wǎng)絡恢復之前,沒有辦法能夠解決

          3.2.4 某個節(jié)點宕機引發(fā)的問題

          還是 A、B、C、D、E 5臺 Redis 實例。客戶端1在 A、B、C 三臺機器上成功加鎖,此時已成功持有 RedLock。

          C 節(jié)點突然宕機,然后重啟,但是客戶端1寫入的數(shù)據(jù)并沒有及時落盤,此時重啟后數(shù)據(jù)丟失。

          最終可能導致客戶端2 成功在 C、D、E 三臺機器加鎖,無法保證鎖的互斥性

          解決方法:

          1. Redis AOF 設置為 fsync=always,通常來說使用 Redis 的用戶不會開啟這個選項,勢必會影響性能,但也是一個可以考慮的選項

          2. 從運維方面解決,保證在 key 的最大 TTL 之后重啟

          3. 代碼優(yōu)化方面的一些建議。盡可能在所有實例上加鎖。但是這并不能完全解決。最好的方法還是方案2。

          3.2.5 其它問題

          使用 RedLock 必須要多搭建一套環(huán)境,比如項目中已經(jīng)使用了 Redis Cluster 或者 Redis Sentinel,為了保證鎖的可靠性必須再搭建一套 RedLock 環(huán)境。在一定程度上,增加了運維成本

          3.3 RedLock 實現(xiàn)

          Redission 也提供了關于 RedLock 的實現(xiàn),各位可參考 Redission 源碼

          4. 總結

          本文基本覆蓋了 Redis 分布式鎖的常見問題與解決方案

          如果對于分布式鎖的互斥性要求并不高的話(例如系統(tǒng)需要計算某個數(shù)據(jù),計算一次即可但是多次計算不會對業(yè)務有影響),傳統(tǒng) Redis 集群(Sentinel,Cluster)做分布式鎖是沒有問題的

          當對于分布式鎖的互斥性有嚴格要求的話,就需要考慮使用 RedLock、Zookeeper、或者數(shù)據(jù)庫寫鎖來解決了

          參考資料
          • https://redis.io/docs/reference/patterns/distributed-locks/
          • https://github.com/redisson/redisson/
          • https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/


          瀏覽 72
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  青青草手机免费在线看片 | 无码影音先锋 | 91亚洲国产精品 | 自拍偷拍在线播放 | 天天干女人在线视频免费观看 |