本文整理自黑馬程式設計師相關資料
在平時單服務的情況下,我們使用互斥鎖可以保證同一時刻只有一個執行緒執行自己的業務。原理是,在JVM內部維護了一個鎖監視器,鎖監視器保證了同一時刻只有一個執行緒獲取到鎖。但是如果開啟了多個服務,就會有多個JVM,從而有多個不同的鎖監視器,每個鎖監視器監視自己JVM內部的執行緒,因此一個JVM內部的執行緒獲取到鎖,並不影響其他JVM內部的執行緒獲取鎖。從而導致並行安全問題。因此,我們需要獨立於JVM之外的鎖監視器對所有的執行緒統一管理。
滿足分散式系統或叢集模式下多程序可見並且互斥的鎖。
MySQL | Redis | Zookeeper | |
---|---|---|---|
互斥 | 利用Mysql本身的互斥鎖機制 | 利用setnx這樣的互斥命令 | 利用節點的唯一性和有序性實現互斥 |
高可用 | 好 | 好 | 好 |
高效能 | 一般 | 好 | 一般 |
安全性 | 斷開連線,自動釋放鎖 | 利用鎖超時時間,到期釋放 | 臨時節點,斷開連線自動釋放 |
獲取鎖:
利用Redis的SETNX保證互斥的特性,同時設定鎖過期時間,避免服務宕機不能執行釋放鎖的操作而導致死鎖。
釋放鎖:
刪除對應的鍵即可
流程圖如下所示:
前面提到的最基本的分散式鎖存在著一些問題。如果獲取鎖的執行緒1阻塞,在該執行緒阻塞期間,鎖超時釋放了,這時執行緒2就可以獲取到鎖,接著執行自己的業務。執行緒1在完成自己的業務後釋放鎖。這時執行緒3也獲得了鎖執行自己的業務,這樣就造成了執行緒2和執行緒3都獲取到了鎖,從而造成了執行緒安全問題。如下圖所示
為了解決未持有鎖的執行緒釋放鎖這個問題,在鎖中存入執行緒標識,在釋放鎖之前先判斷鎖標識是否是本身執行緒。如果標識是自己,則釋放鎖。其流程圖如下所示
由於前面加入了判斷,判斷與釋放是兩步。有可能在判斷時持有鎖的執行緒1阻塞,直到超時釋放鎖,執行緒2拿到了鎖,執行緒1被喚醒並執行釋放鎖,導致執行緒3也拿到了鎖。造成了兩個執行緒同時持有鎖的執行緒安全問題。如下所示
為了解決這個問題,使用Lua指令碼,在一個指令碼中編寫多條Redis命令,確保多條命令執行時的原子性。
釋放鎖的業務流程如下所示
-- 這裡的 KEYS[1] 就是鎖的key,這裡的ARGV[1] 就是當前執行緒標示
-- 獲取鎖中的標示,判斷是否與當前執行緒標示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,則刪除鎖
return redis.call('DEL', KEYS[1])
end
-- 不一致,則直接返回
return 0
到目前為止,一個基於Redis的基本的分散式鎖就完成了。但還是存在著以下問題
獲取鎖的Lua指令碼
local key = KEYS[1]; -- 鎖的key
local threadId = ARGV[1]; -- 執行緒唯一標識
local releaseTime = ARGV[2]; -- 鎖的自動釋放時間
-- 判斷是否存在
if(redis.call('exists', key) == 0) then
-- 不存在, 獲取鎖
redis.call('hset', key, threadId, '1');
-- 設定有效期
redis.call('expire', key, releaseTime);
return 1; -- 返回結果
end;
-- 鎖已經存在,判斷threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
-- 不存在, 獲取鎖,重入次數+1
redis.call('hincrby', key, threadId, '1');
-- 設定有效期
redis.call('expire', key, releaseTime);
return 1; -- 返回結果
end;
return 0; -- 程式碼走到這裡,說明獲取鎖的不是自己,獲取鎖失敗
釋放鎖的Lua指令碼
local key = KEYS[1]; -- 鎖的key
local threadId = ARGV[1]; -- 執行緒唯一標識
local releaseTime = ARGV[2]; -- 鎖的自動釋放時間
-- 判斷當前鎖是否還是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then
return nil; -- 如果已經不是自己,則直接返回
end;
-- 是自己的鎖,則重入次數-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判斷是否重入次數是否已經為0
if (count > 0) then
-- 大於0說明不能釋放鎖,重置有效期然後返回
redis.call('EXPIRE', key, releaseTime);
return nil;
else -- 等於0說明可以釋放鎖,直接刪除
redis.call('DEL', key);
return nil;
end;