<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 分布式鎖的 5 種方案

          共 10884字,需瀏覽 22分鐘

           ·

          2023-11-02 03:26

          本地加鎖的方式在分布式的場景下不適用,所以本文我們來探討下如何引入分布式鎖解決本地鎖的問題。本篇所有代碼和業(yè)務基于我的開源項目 PassJava。

          本篇主要內容如下:

          一、本地鎖的問題

          首先我們來回顧下本地鎖的問題:

          目前題目微服務被拆分成了四個微服務。前端請求進來時,會被轉發(fā)到不同的微服務。假如前端接收了 10 W 個請求,每個微服務接收 2.5 W 個請求,假如緩存失效了,每個微服務在訪問數(shù)據庫時加鎖,通過鎖(synchronziedlock)來鎖住自己的線程資源,從而防止緩存擊穿

          這是一種本地加鎖的方式,在分布式情況下會帶來數(shù)據不一致的問題:比如服務 A 獲取數(shù)據后,更新緩存 key =100,服務 B 不受服務 A 的鎖限制,并發(fā)去更新緩存 key = 99,最后的結果可能是 99 或 100,但這是一種未知的狀態(tài),與期望結果不一致。流程圖如下所示:

          二、什么是分布式鎖

          基于上面本地鎖的問題,我們需要一種支持分布式集群環(huán)境下的鎖:查詢 DB 時,只有一個線程能訪問,其他線程都需要等待第一個線程釋放鎖資源后,才能繼續(xù)執(zhí)行。

          生活中的案例:可以把鎖看成房門外的一把,所有并發(fā)線程比作,他們都想進入房間,房間內只能有一個人進入。當有人進入后,將門反鎖,其他人必須等待,直到進去的人出來。

          我們來看下分布式鎖的基本原理,如下圖所示:

          我們來分析下上圖的分布式鎖:

          • 1.前端將 10W 的高并發(fā)請求轉發(fā)給四個題目微服務。
          • 2.每個微服務處理 2.5 W 個請求。
          • 3.每個處理請求的線程在執(zhí)行業(yè)務之前,需要先搶占鎖。可以理解為“占坑”。
          • 4.獲取到鎖的線程在執(zhí)行完業(yè)務后,釋放鎖。可以理解為“釋放坑位”。
          • 5.未獲取到的線程需要等待鎖釋放。
          • 6.釋放鎖后,其他線程搶占鎖。
          • 7.重復執(zhí)行步驟 4、5、6。

          大白話解釋:所有請求的線程都去同一個地方“占坑”,如果有坑位,就執(zhí)行業(yè)務邏輯,沒有坑位,就需要其他線程釋放“坑位”。這個坑位是所有線程可見的,可以把這個坑位放到 Redis 緩存或者數(shù)據庫,這篇講的就是如何用 Redis 做“分布式坑位”

          三、Redis 的 SETNX

          Redis 作為一個公共可訪問的地方,正好可以作為“占坑”的地方。

          用 Redis 實現(xiàn)分布式鎖的幾種方案,我們都是用 SETNX 命令(設置 key 等于某 value)。只是高階方案傳的參數(shù)個數(shù)不一樣,以及考慮了異常情況。

          我們來看下這個命令,SETNXset If not exist的簡寫。意思就是當 key 不存在時,設置 key 的值,存在時,什么都不做。

          在 Redis 命令行中是這樣執(zhí)行的:

          set <key> <value> NX

          我們可以進到 redis 容器中來試下 SETNX 命令。

          先進入容器:

          docker exec -it <容器 id> redid-cli

          然后執(zhí)行 SETNX 命令:將 wukong 這個 key 對應的 value 設置成 1111

          set wukong 1111 NX

          返回 OK,表示設置成功。重復執(zhí)行該命令,返回 nil表示設置失敗。

          四、青銅方案

          我們先用 Redis 的 SETNX 命令來實現(xiàn)最簡單的分布式鎖。

          3.1 青銅原理

          我們來看下流程圖:

          • 多個并發(fā)線程都去 Redis 中申請鎖,也就是執(zhí)行 setnx 命令,假設線程 A 執(zhí)行成功,說明當前線程 A 獲得了。
          • 其他線程執(zhí)行 setnx 命令都會是失敗的,所以需要等待線程 A 釋放鎖。
          • 線程 A 執(zhí)行完自己的業(yè)務后,刪除鎖。
          • 其他線程繼續(xù)搶占鎖,也就是執(zhí)行 setnx 命令。因為線程 A 已經刪除了鎖,所以又有其他線程可以搶占到鎖了。

          代碼示例如下,Java 中 setnx 命令對應的代碼為 setIfAbsent

          setIfAbsent 方法的第一個參數(shù)代表 key,第二個參數(shù)代表值。

          // 1.先搶占鎖
          Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock""123");
          if(lock) {
            // 2.搶占成功,執(zhí)行業(yè)務
            List<TypeEntity> typeEntityListFromDb = getDataFromDB();
            // 3.解鎖
            redisTemplate.delete("lock");
            return typeEntityListFromDb;
          else {
            // 4.休眠一段時間
            sleep(100);
            // 5.搶占失敗,等待鎖釋放
            return getTypeEntityListByRedisDistributedLock();
          }

          一個小問題:那為什么需要休眠一段時間?

          因為該程序存在遞歸調用,可能會導致棧空間溢出。

          3.2 青銅方案的缺陷

          青銅之所以叫青銅,是因為它是最初級的,肯定會帶來很多問題。

          設想一種家庭場景:晚上小空一個人開鎖進入了房間,打開了電燈??,然后突然斷電了,小空想開門出去,但是找不到門鎖位置,那小明就進不去了,外面的人也進不來。

          從技術的角度看:setnx 占鎖成功,業(yè)務代碼出現(xiàn)異常或者服務器宕機,沒有執(zhí)行刪除鎖的邏輯,就造成了死鎖

          那如何規(guī)避這個風險呢?

          設置鎖的自動過期時間,過一段時間后,自動刪除鎖,這樣其他線程就能獲取到鎖了。

          四、白銀方案

          4.1 生活中的例子

          上面提到的青銅方案會有死鎖問題,那我們就用上面的規(guī)避風險的方案來設計下,也就是我們的白銀方案。

          還是生活中的例子:小空開鎖成功后,給這款智能鎖設置了一個沙漏倒計時?,沙漏完后,門鎖自動打開。即使房間突然斷電,過一段時間后,鎖會自動打開,其他人就可以進來了。

          4.2 技術原理圖

          和青銅方案不同的地方在于,在占鎖成功后,設置鎖的過期時間,這兩步是分步執(zhí)行的。如下圖所示:

          4.3 示例代碼

          清理 redis key 的代碼如下

          // 在 10s 以后,自動清理 lock
          redisTemplate.expire("lock"10, TimeUnit.SECONDS);

          完整代碼如下:

          // 1.先搶占鎖
          Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock""123");
          if(lock) {
              // 2.在 10s 以后,自動清理 lock
              redisTemplate.expire("lock"10, TimeUnit.SECONDS);
              // 3.搶占成功,執(zhí)行業(yè)務
              List<TypeEntity> typeEntityListFromDb = getDataFromDB();
              // 4.解鎖
              redisTemplate.delete("lock");
              return typeEntityListFromDb;
          }

          4.4 白銀方案的缺陷

          白銀方案看似解決了線程異常或服務器宕機造成的鎖未釋放的問題,但還是存在其他問題:

          因為占鎖和設置過期時間是分兩步執(zhí)行的,所以如果在這兩步之間發(fā)生了異常,則鎖的過期時間根本就沒有設置成功。

          所以和青銅方案有一樣的問題:鎖永遠不能過期

          五、黃金方案

          5.1 原子指令

          上面的白銀方案中,占鎖和設置鎖過期時間是分步兩步執(zhí)行的,這個時候,我們可以聯(lián)想到什么:事務的原子性(Atom)。

          原子性:多條命令要么都成功執(zhí)行,要么都不執(zhí)行。

          將兩步放在一步中執(zhí)行:占鎖+設置鎖過期時間。

          Redis 正好支持這種操作:

          # 設置某個 key 的值并設置多少毫秒或秒 過期。
          set <key> <value> PX <多少毫秒> NX

          set <key> <value> EX <多少秒> NX

          然后可以通過如下命令查看 key 的變化

          ttl <key>

          下面演示下如何設置 key 并設置過期時間。注意:執(zhí)行命令之前需要先刪除 key,可以通過客戶端或命令刪除。

          # 設置 key=wukong,value=1111,過期時間=5000ms
          set wukong 1111 PX 5000 NX
          # 查看 key 的狀態(tài)
          ttl wukong

          執(zhí)行結果如下圖所示:每運行一次 ttl 命令,就可以看到 wukong 的過期時間就會減少。最后會變?yōu)?-2(已過期)。

          5.2 技術原理圖

          黃金方案和白銀方案的不同之處:獲取鎖的時候,也需要設置鎖的過期時間,這是一個原子操作,要么都成功執(zhí)行,要么都不執(zhí)行。如下圖所示:

          5.3 示例代碼

          設置 lock 的值等于 123,過期時間為 10 秒。如果 10 秒 以后,lock 還存在,則清理 lock。

          setIfAbsent("lock""123"10, TimeUnit.SECONDS);

          5.4 黃金方案的缺陷

          我們還是舉生活中的例子來看下黃金方案的缺陷。

          5.4.1 用戶 A 搶占鎖

          • 用戶 A 先搶占到了鎖,并設置了這個鎖 10 秒以后自動開鎖,鎖的編號為 123
          • 10 秒以后,A 還在執(zhí)行任務,此時鎖被自動打開了。

          5.4.2 用戶 B 搶占鎖

          • 用戶 B 看到房間的鎖打開了,于是搶占到了鎖,設置鎖的編號為 123,并設置了過期時間 10 秒
          • 因房間內只允許一個用戶執(zhí)行任務,所以用戶 A 和 用戶 B 執(zhí)行任務產生了沖突
          • 用戶 A 在 15 s 后,完成了任務,此時 用戶 B 還在執(zhí)行任務。
          • 用戶 A 主動打開了編號為 123的鎖。
          • 用戶 B 還在執(zhí)行任務,發(fā)現(xiàn)鎖已經被打開了。
          • 用戶 B 非常生氣:我還沒執(zhí)行完任務呢,鎖怎么開了?

          5.4.3 用戶 C 搶占鎖

          • 用戶 B 的鎖被 A 主動打開后,A 離開房間,B 還在執(zhí)行任務。
          • 用戶 C 搶占到鎖,C 開始執(zhí)行任務。
          • 因房間內只允許一個用戶執(zhí)行任務,所以用戶 B 和 用戶 C 執(zhí)行任務產生了沖突。

          從上面的案例中我們可以知道,因為用戶 A 處理任務所需要的時間大于鎖自動清理(開鎖)的時間,所以在自動開鎖后,又有其他用戶搶占到了鎖。當用戶 A 完成任務后,會把其他用戶搶占到的鎖給主動打開。

          這里為什么會打開別人的鎖?因為鎖的編號都叫做 “123”,用戶 A 只認鎖編號,看見編號為 “123”的鎖就開,結果把用戶 B 的鎖打開了,此時用戶 B 還未執(zhí)行完任務,當然生氣了。

          六、鉑金方案

          6.1 生活中的例子

          上面的黃金方案的缺陷也很好解決,給每個鎖設置不同的編號不就好了~

          如下圖所示,B 搶占的鎖是藍色的,和 A 搶占到綠色鎖不一樣。這樣就不會被 A 打開了。

          做了個動圖,方便理解:

          動圖演示

          靜態(tài)圖更高清,可以看看:

          6.2 技術原理圖

          與黃金方案的不同之處:

          • 設置鎖的過期時間時,還需要設置唯一編號。
          • 主動刪除鎖的時候,需要判斷鎖的編號是否和設置的一致,如果一致,則認為是自己設置的鎖,可以進行主動刪除。

          6.3 代碼示例

          // 1.生成唯一 id
          String uuid = UUID.randomUUID().toString();
          // 2. 搶占鎖
          Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
          if(lock) {
              System.out.println("搶占成功:" + uuid);
              // 3.搶占成功,執(zhí)行業(yè)務
              List<TypeEntity> typeEntityListFromDb = getDataFromDB();
              // 4.獲取當前鎖的值
              String lockValue = redisTemplate.opsForValue().get("lock");
              // 5.如果鎖的值和設置的值相等,則清理自己的鎖
              if(uuid.equals(lockValue)) {
                  System.out.println("清理鎖:" + lockValue);
                  redisTemplate.delete("lock");
              }
              return typeEntityListFromDb;
          else {
              System.out.println("搶占失敗,等待鎖釋放");
              // 4.休眠一段時間
              sleep(100);
              // 5.搶占失敗,等待鎖釋放
              return getTypeEntityListByRedisDistributedLock();
          }
          • 1.生成隨機唯一 id,給鎖加上唯一值。
          • 2.搶占鎖,并設置過期時間為 10 s,且鎖具有隨機唯一 id。
          • 3.搶占成功,執(zhí)行業(yè)務。
          • 4.執(zhí)行完業(yè)務后,獲取當前鎖的值。
          • 5.如果鎖的值和設置的值相等,則清理自己的鎖。

          6.4 鉑金方案的缺陷

          上面的方案看似很完美,但還是存在問題:第 4 步和第 5 步并不是原子性的。

          • 時刻:0s。線程 A 搶占到了鎖。

          • 時刻:9.5s。線程 A 向 Redis 查詢當前 key 的值。

          • 時刻:10s。鎖自動過期。

          • 時刻:11s。線程 B 搶占到鎖。

          • 時刻:12s。線程 A 在查詢途中耗時長,終于拿多鎖的值。

          • 時刻:13s。線程 A 還是拿自己設置的鎖的值和返回的值進行比較,值是相等的,清理鎖,但是這個鎖其實是線程 B 搶占的鎖。

          那如何規(guī)避這個風險呢?鉆石方案登場。

          七、鉆石方案

          上面的線程 A 查詢鎖和刪除鎖的邏輯不是原子性的,所以將查詢鎖和刪除鎖這兩步作為原子指令操作就可以了。

          7.1 技術原理圖

          如下圖所示,紅色圈出來的部分是鉆石方案的不同之處。用腳本進行刪除,達到原子操作。

          7.2 代碼示例

          那如何用腳本進行刪除呢?

          我們先來看一下這段 Redis 專屬腳本:

          if redis.call("get",KEYS[1]) == ARGV[1]
          then
              return redis.call("del",KEYS[1])
          else
              return 0
          end

          這段腳本和鉑金方案的獲取key,刪除key的方式很像。先獲取 KEYS[1] 的 value,判斷 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,則刪除 KEYS[1]。

          那么這段腳本怎么在 Java 項目中執(zhí)行呢?

          分兩步:先定義腳本;用 redisTemplate.execute 方法執(zhí)行腳本。

          // 腳本解鎖
          String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
          redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

          上面的代碼中,KEYS[1] 對應“l(fā)ock”,ARGV[1] 對應 “uuid”,含義就是如果 lock 的 value 等于 uuid 則刪除 lock。

          而這段 Redis 腳本是由 Redis 內嵌的 Lua 環(huán)境執(zhí)行的,所以又稱作 Lua 腳本。

          那鉆石方案是不是就完美了呢?有沒有更好的方案呢?

          之后我們再來介紹另外一種分布式鎖的王者方案:Redisson。可以參考:最強分布式鎖工具:Redisson

          八、總結

          本篇通過本地鎖的問題引申出分布式鎖的問題。然后介紹了五種分布式鎖的方案,由淺入深講解了不同方案的改進之處。

          從上面幾種方案的不斷演進的過程中,知道了系統(tǒng)中哪些地方可能存在異常情況,以及該如何更好地進行處理。

          舉一反三,這種不斷演進的思維模式也可以運用到其他技術中。

          下面總結下上面五種方案的缺陷和改進之處。

          青銅方案

          • 缺陷:業(yè)務代碼出現(xiàn)異常或者服務器宕機,沒有執(zhí)行主動刪除鎖的邏輯,就造成了死鎖。
          • 改進:設置鎖的自動過期時間,過一段時間后,自動刪除鎖,這樣其他線程就能獲取到鎖了。

          白銀方案

          • 缺陷:占鎖和設置鎖過期時間是分步兩步執(zhí)行的,不是原子操作。
          • 改進:占鎖和設置鎖過期時間保證原子操作。

          黃金方案

          • 缺陷:主動刪除鎖時,因鎖的值都是相同的,將其他客戶端占用的鎖刪除了。
          • 改進:每次占用的鎖,隨機設為較大的值,主動刪除鎖時,比較鎖的值和自己設置的值是否相等。

          鉑金方案

          • 缺陷:獲取鎖、比較鎖的值、刪除鎖,這三步是非原子性的。中途又可能鎖自動過期了,又被其他客戶端搶占了鎖,導致刪鎖時把其他客戶端占用的鎖刪了。
          • 改進:使用 Lua 腳本進行獲取鎖、比較鎖、刪除鎖的原子操作。

          鉆石方案

          • 缺陷:非專業(yè)的分布式鎖方案。
          • 改進:Redission 分布式鎖。
               
               

          程序汪資料鏈接

          程序汪接的7個私活都在這里,經驗整理

          Java項目分享  最新整理全集,找項目不累啦 07版

          堪稱神級的Spring Boot手冊,從基礎入門到實戰(zhàn)進階

          臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!

          臥槽!阿里大佬總結的《圖解Java》火了,完整版PDF開放下載!

          字節(jié)跳動總結的設計模式 PDF 火了,完整版開放下載!

          歡迎添加程序汪個人微信 itwang007  進粉絲群或圍觀朋友圈

          瀏覽 1304
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  欧美内射视频 | 天天操天天操天天干 | 日本黄色视频。 | 北条麻妃中文 | 久久亚洲天堂 |