通過 Redis 分佈式鎖的實現

分佈式鎖實現的三個核心要素:

  • 加鎖
  • 解鎖
  • 鎖超時

加鎖

最簡單的方法是使用 setnx命令。key是鎖的唯一標識,按業務來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給 key命名爲lock_sale_商品ID。而 value 設置成什麼呢?我們可以姑且設置成 1。加鎖的僞代碼如下:

setnx(lock_sale_商品ID,1

當一個線程執行setnx返回 1,說明 key原本不存在,該線程成功得到了鎖;當一個線程執行 setnx返回0,說明key已經存在,該線程搶鎖失敗。

解鎖

有加鎖就得有解鎖。當得到鎖的線程執行完任務,需要釋放鎖,以便其他線程可以進入。釋放鎖的最簡單方式是執行del指令,僞代碼如下:

del(lock_sale_商品ID)

釋放鎖之後,其他線程就可以繼續執行 setnx命令來獲得鎖。

鎖超時

鎖超時是什麼意思呢?如果一個得到鎖的線程在執行任務的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住(死鎖),別的線程再也別想進來。所以,setnxkey必須設置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放。setnx不支持超時參數,所以需要額外的指令,僞代碼如下:

expire(lock_sale_商品ID, 30

綜合僞代碼如下:

if(setnx(lock_sale_商品ID,1== 1{
    expire(lock_sale_商品ID,30try {
        do something ......
    } finally {
        del(lock_sale_商品ID)
    }
}

存在什麼問題

以上僞代碼中存在三個致命問題

setnxexpire 的非原子性

設想一個極端場景,當某線程執行setnx,成功得到了鎖:
在這裏插入圖片描述
setnx剛執行成功,還未來得及執行expire 指令,節點 1 掛掉了。
在這裏插入圖片描述
這樣一來,這把鎖就沒有設置過期時間,變成死鎖,別的線程再也無法獲得鎖了。
怎麼解決呢?setnx指令本身是不支持傳入超時時間的,set 指令增加了可選參數,僞代碼如下:

set(lock_sale_商品ID,130,NX)

這樣就可以取代 setnx指令。

del 導致誤刪

又是一個極端場景,假如某線程成功得到了鎖,並且設置的超時時間是 30 秒。
在這裏插入圖片描述
如果某些原因導致線程 A 執行的很慢很慢,過了 30 秒都沒執行完,這時候鎖過期自動釋放,線程 B 得到了鎖。
在這裏插入圖片描述
隨後,線程 A 執行完了任務,線程 A 接着執行 del 指令來釋放鎖。但這時候線程 B 還沒執行完,線程A實際上刪除的是線程 B 加的鎖
在這裏插入圖片描述
怎麼避免這種情況呢?可以在 del釋放鎖之前做一個判斷,驗證當前的鎖是不是自己加的鎖。至於具體的實現,可以在加鎖的時候把當前的線程 ID 當做 value,並在刪除之前驗證 key對應的 value是不是自己線程的ID

加鎖:

String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)

解鎖:

if(threadId .equals(redisClient.get(key)){
    del(key)
}

但是,這樣做又隱含了一個新的問題,判斷和釋放鎖是兩個獨立操作,不是原子性。

出現併發的可能性

還是剛纔第二點所描述的場景,雖然我們避免了線程 A 誤刪掉 key 的情況,但是同一時間有 A,B 兩個線程在訪問代碼塊,仍然是不完美的。怎麼辦呢?我們可以讓獲得鎖的線程開啓一個守護線程,用來給快要過期的鎖“續航”。
在這裏插入圖片描述
當過去了 29 秒,線程 A 還沒執行完,這時候守護線程會執行 expire 指令,爲這把鎖“續命”20 秒。守護線程從第 29 秒開始執行,每 20 秒執行一次。
在這裏插入圖片描述
當線程 A 執行完任務,會顯式關掉守護線程。
在這裏插入圖片描述
另一種情況,如果節點 1 忽然斷電,由於線程 A 和守護線程在同一個進程,守護線程也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章