布式鎖實現的三個核心要素:加鎖、解鎖、鎖超時。
一、Redis實現分布式鎖基本原理
1.1、Redis具體實現
情景:存在多台JVM需要同時對某一商品(id)進行操作。
-
加鎖:使用setnx命令,偽代碼:setnx(id,value)
返回1,說明key不存在,線程搶鎖成功;
返回1,說明key已存在,線程搶鎖失敗。
注意:setnx(id,value)中key為操作商品的id,value值可用於防止誤刪鎖,下文有提到。 -
解鎖:使用del命令,偽代碼:del(id)
-
鎖超時:如果一個得到鎖的線程在執行任務的過程中掛掉,來不及顯式地釋放鎖,該資源將會被永遠占用,其他線程將無法訪問。
可以使用expire為key設置一個超時時間,與setnx命令一起執行(setnx不支持超時參數),用以保證即使未被顯式釋放,該鎖也可在一定時間后自動釋放。偽代碼:expire(key, 30)。

1.2、上述實現存在的問題
-
非原子性操作
加鎖setnx和鎖超時expire兩個命令未非原子性操作,當執行加鎖setnx后,若因網絡或客戶端問題鎖超時expire命令未成功執行時,鎖將無法被釋放。
解決方案:
使用set命令取代setnx和expire命令。setnx本身不支持設置超時時間。在Redis 2.6.12以上版本為set指令增加了可選參數,偽代碼:set(key, value, expire)。 -
誤刪鎖
設想如下情形:
(1)JVM1使用set(001, 002, 30)成功獲取鎖,並設置超時時間為30s;
(2)JVM1開始數據處理,處理時間已經超過了30s...
(3)服務器檢測到(001, 002, 30)數據超時,將自動執行del進行數據刪除,此時JVM1還在數據處理...
(4)此時,JVM2使用set(001, 002, 30)成功獲取鎖,並設置超時時間為30s;
(5)JVM2開始數據處理。與此同時,JVM1處理完成,操作提交后,根據商品id001,執行了del;
到此,JVM1成功誤刪了JVM2的鎖。
解決方案:
del數據之前,增加鎖判斷機制:判斷要刪除的鎖是否屬於本線程。操作流程:
(1)加鎖:set(id, threadId,expire),其中value為當前線程ID;
(2)解鎖:執行del命令時,根據id和threadId數據判斷該鎖是否仍屬於本線程。是,則刪除。 -
並發問題
基於誤刪鎖的前提下,由於我們無法確定程序成功處理完成數據的具體時間,這就為超時時間的設置提出了難題。設置時間過長、過短都將影響程序並發的效率。
解決方案:JVM1需要自己判斷在超時時間內是否完成數據處理,如未完成,應請求延長超時時間。具體操作:
為獲取鎖的鎖的線程開啟一個守護線程。當29秒時(或更早),線程A還沒執行完,守護線程會執行expire指令,為這把鎖“續命”20秒。守護線程從第29秒開始執行,每20秒執行一次。當線程A執行完任務,會顯式關掉守護線程。

另一種情況:如果節點1 忽然斷電,由於線程A和守護線程在同一個進程,守護線程也會停下。當過了超時時間后,沒有守護進程的“續命”,鎖將自動釋放。
出處: https://www.cnblogs.com/DeepInThought
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
