<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 分布式鎖|從青銅到鉆石的五種演進(jìn)方案

          共 8767字,需瀏覽 18分鐘

           ·

          2021-09-17 13:32

          中秋佳節(jié),文末抽獎送書啦~

          上篇我們講到如何用本地內(nèi)存:《緩存實(shí)戰(zhàn)(上篇)》 來做緩存從而增強(qiáng)系統(tǒng)的性能,另外探討了加鎖解決緩存擊穿的問題。但是本地加鎖的方式在分布式的場景下就不適用了,所以本文我們來探討下如何引入分布式鎖解決本地鎖的問題。本篇所有代碼和業(yè)務(wù)基于我的開源項(xiàng)目 PassJava。

          本篇主要內(nèi)容如下:

          一、本地鎖的問題

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

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

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

          二、什么是分布式鎖

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

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

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

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

          • 1.前端將 10W 的高并發(fā)請求轉(zhuǎn)發(fā)給四個題目微服務(wù)。
          • 2.每個微服務(wù)處理 2.5 W 個請求。
          • 3.每個處理請求的線程在執(zhí)行業(yè)務(wù)之前,需要先搶占鎖??梢岳斫鉃椤罢伎印薄?/section>
          • 4.獲取到鎖的線程在執(zhí)行完業(yè)務(wù)后,釋放鎖??梢岳斫鉃椤搬尫趴游弧?。
          • 5.未獲取到的線程需要等待鎖釋放。
          • 6.釋放鎖后,其他線程搶占鎖。
          • 7.重復(fù)執(zhí)行步驟 4、5、6。

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

          三、Redis 的 SETNX

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

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

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

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

          set <key> <value> NX

          我們可以進(jìn)到 redis 容器中來試下 SETNX 命令。

          先進(jìn)入容器:

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

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

          set wukong 1111 NX

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

          四、青銅方案

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

          3.1 青銅原理

          我們來看下流程圖:

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

          代碼示例如下,Java 中 setnx 命令對應(yīng)的代碼為 setIfAbsent。

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

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

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

          因?yàn)樵摮绦虼嬖谶f歸調(diào)用,可能會導(dǎo)致??臻g溢出。

          3.2 青銅方案的缺陷

          青銅之所以叫青銅,是因?yàn)樗亲畛跫壍?,肯定會帶來很多問題。

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

          從技術(shù)的角度看:setnx 占鎖成功,業(yè)務(wù)代碼出現(xiàn)異?;蛘叻?wù)器宕機(jī),沒有執(zhí)行刪除鎖的邏輯,就造成了死鎖。

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

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

          四、白銀方案

          4.1 生活中的例子

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

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

          4.2 技術(shù)原理圖

          和青銅方案不同的地方在于,在占鎖成功后,設(shè)置鎖的過期時間,這兩步是分步執(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è)務(wù)
              List<TypeEntity> typeEntityListFromDb = getDataFromDB();
              // 4.解鎖
              redisTemplate.delete("lock");
              return typeEntityListFromDb;
          }

          4.4 白銀方案的缺陷

          白銀方案看似解決了線程異?;蚍?wù)器宕機(jī)造成的鎖未釋放的問題,但還是存在其他問題:

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

          所以和青銅方案有一樣的問題:鎖永遠(yuǎn)不能過期。

          五、黃金方案

          5.1 原子指令

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

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

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

          Redis 正好支持這種操作:

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

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

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

          ttl <key>

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

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

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

          5.2 技術(shù)原理圖

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

          5.3 示例代碼

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

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

          5.4 黃金方案的缺陷

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

          5.4.1 用戶 A 搶占鎖

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

          5.4.2 用戶 B 搶占鎖

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

          5.4.3 用戶 C 搶占鎖

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

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

          這里為什么會打開別人的鎖?因?yàn)殒i的編號都叫做 “123”,用戶 A 只認(rèn)鎖編號,看見編號為 “123”的鎖就開,結(jié)果把用戶 B 的鎖打開了,此時用戶 B 還未執(zhí)行完任務(wù),當(dāng)然生氣了。

          六、鉑金方案

          6.1 生活中的例子

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

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

          做了個動圖,方便理解:

          動圖演示

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

          6.2 技術(shù)原理圖

          與黃金方案的不同之處:

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

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

          6.4 鉑金方案的缺陷

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

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

          • 時刻:9.5s。線程 A 向 Redis 查詢當(dāng)前 key 的值。

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

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

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

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

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

          七、鉆石方案

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

          7.1 技術(shù)原理圖

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

          7.2 代碼示例

          那如何用腳本進(jìn)行刪除呢?

          我們先來看一下這段 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 項(xiàng)目中執(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] 對應(yīng)“l(fā)ock”,ARGV[1] 對應(yīng) “uuid”,含義就是如果 lock 的 value 等于 uuid 則刪除 lock。

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

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

          下篇,我們再來介紹另外一種分布式鎖的王者方案:Redisson。

          八、總結(jié)

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

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

          舉一反三,這種不斷演進(jìn)的思維模式也可以運(yùn)用到其他技術(shù)中。

          下面總結(jié)下上面五種方案的缺陷和改進(jìn)之處。

          青銅方案

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

          白銀方案

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

          黃金方案

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

          鉑金方案

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

          鉆石方案

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

          -- End --

          中秋節(jié)來了,抽獎贈書福利啦~~~
          贈送新書Spring Cloud Alibaba微服務(wù)實(shí)戰(zhàn)共3本,這是一本Java語言新書籍,剛剛上架!由「 北京大學(xué)出版社」贊助提供 ,感興趣的朋友推薦入手一本。
          本書介紹了微服務(wù)的基礎(chǔ)理論和配置基礎(chǔ)開發(fā)環(huán)境,以及如何構(gòu)建Spring Cloud Alibaba 模板項(xiàng)目;使用Spring Cloud Alibaba套件(Nacos、Sentinel等)和當(dāng)下流行的微服務(wù)組件(如Spring Cloud Gateway、Spring Cloud Stream等)搭建微服務(wù)系統(tǒng),解決開發(fā)中的常見問題,其中不乏有一些筆者對編寫代碼的個人見解;介紹了使用Jenkins + Gitlab + Docker部署服務(wù),如同流水線生產(chǎn)一樣,使部署服務(wù)又快又穩(wěn),從而使讀者能夠了解程序由開發(fā)到上線的整體流程。

          抽獎方式:評論區(qū)留言,點(diǎn)贊最多或者最走心的評論,選取 3 位!

          留言區(qū)見

          瀏覽 64
          點(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>
                  国产精品久久久久久久曹县翰林府 | 久草大香蕉视频在线 | 做爱网站| 福利资源导航 | 男女高清无码 |