分布式鎖
本文整理自黑馬程式員相關資料
問題的引入
在平時單服務的情況下,我們使用互斥鎖可以保證同一時刻只有一個執行緒執行自己的業務,原理是,在JVM內部維護了一個鎖監視器,鎖監視器保證了同一時刻只有一個執行緒獲取到鎖,但是如果開啟了多個服務,就會有多個JVM,從而有多個不同的鎖監視器,每個鎖監視器監視自己JVM內部的執行緒,因此一個JVM內部的執行緒獲取到鎖,并不影響其他JVM內部的執行緒獲取鎖,從而導致并發安全問題,因此,我們需要獨立于JVM之外的鎖監視器對所有的執行緒統一管理,

概念
滿足分布式系統或集群模式下多行程可見并且互斥的鎖,
常見分布式鎖的實作比較
| MySQL | Redis | Zookeeper | |
|---|---|---|---|
| 互斥 | 利用Mysql本身的互斥鎖機制 | 利用setnx這樣的互斥命令 | 利用節點的唯一性和有序性實作互斥 |
| 高可用 | 好 | 好 | 好 |
| 高性能 | 一般 | 好 | 一般 |
| 安全性 | 斷開連接,自動釋放鎖 | 利用鎖超時時間,到期釋放 | 臨時節點,斷開連接自動釋放 |
基于Redis的分布式鎖
最基本的分布式鎖
獲取鎖:
利用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的基本的分布式鎖就完成了,但還是存在著以下問題
- 不可重入:同一線城無法多次獲取統一把鎖
- 不可重試:獲取鎖只嘗試一次就回傳,沒有重試機制
- 超時釋放問題:鎖超時釋放雖然可以避免死鎖,但是如果業務執行耗時較長,也會導致鎖釋放,存在安全隱患
- 主從一致性問題:如果Redis提供了主從集群,主從同步存在延遲,當主節點宕機時,從節點沒有同步主節點中的鎖資料,其他執行緒就會拿到鎖
Redisson分布式鎖簡單介紹
Redisson可重入鎖原理

獲取鎖的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;
Redisson分布式鎖原理
- 可重入:利用hash結構記錄執行緒id和重入次數
- 可重試:利用信號量和PubSub功能實作等待、喚醒,獲取鎖失敗的重試機制
- 超時續約:利用watchDog,每隔一段時間(releaseTime/3),重置超時時間,

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/499128.html
標籤:NoSQL
上一篇:快取穿透,快取雪崩,快取擊穿
