一、前言
在談 分佈式鎖
之前不得不聊下 線程鎖
、 進程鎖
:
- 線程鎖:在編程時給方法、代碼塊加鎖,使得在同一時刻只能有一線程執行此方法、代碼塊保證線程安全。
- 進程鎖:控制在同一操作系統中多個進程同時訪問一個共享資源,只是因爲程序的獨立性。
(一)介紹
分佈式鎖是當多個進程不在統一操作系統中,控制分佈式系統或不同系統之間共同訪問共享資源的一種鎖實現,如果不同的系統或同一個系統的不同主機之間共享了某個資源時,往往需要互斥來防止彼此干擾來保證一致性。
(二)分佈式鎖需要具備哪些條件
- 互斥性:在任意一個時刻,只有一個客戶端持有鎖。
- 無死鎖:即便持有鎖的客戶端崩潰或者其他意外事件,鎖仍然可以被獲取。
- 容錯:只要大部分
Redis
節點都活着,客戶端就可以獲取和釋放鎖
(三)分佈式鎖的實現有哪些?
- 數據庫
Redis
(setnx
命令)Memcached
(add
命令)- …
二、實現
在實現之前筆者也是網上參考了一些成熟方案的實現,基本都是用 Redis
實現,畢竟 redis的高性能是有目共睹的,ok,講下實現的一個思路:
- 獲取鎖的時候,使用
setnx
添加一個鎖,並使用expire
命令給鎖加個超時時間,超過該時間自動釋放,鎖的value
值爲隨機生成Guid
,釋放鎖時驗證。- 當不需要鎖時,要進行釋放鎖,釋放時需要驗證鎖的
value
值釋放正確,避免釋放了別人的鎖。
ps:
- 超時時間是避免程序奔潰導致死鎖,別的進程無法正常獲取鎖。
- 不論獲取鎖、釋放鎖都必須具備
原子性
,避免死鎖的可能,比如:獲取鎖,分步驟先setnx
加鎖,再使用expire
加超時時間,那就很有可能還沒設置超時時間之前系統宕機導致死鎖。
考慮原子性問題,直接使用 Lua
腳本:
1 獲取鎖
local gva = redis.call('GET', KEYS[1])
if gva == ARGV[1] then
local ttlva = redis.call('PTTL', KEYS[1])
redis.call('PEXPIRE', KEYS[1], ARGV[2] + ttlva)
return 1
end
return 0
2 釋放鎖
local gva = redis.call('GET', KEYS[1])
if gva == ARGV[1] then
redis.call('DEL', KEYS[1])
return 1
end
return 0
Lua
語法還是比較簡單,不需要具體的學習也能看個大概。
三、總結
- 獲取鎖、釋放鎖操作分別都需要具備
原子性
。 - 加鎖時 key相同、value不同。
- 釋放鎖時,根據value判斷,是否是自己的鎖,不能是否他人鎖。
- 業務結束時,及時釋放鎖,不能利用超時才進行釋放鎖。
- 鎖的超時時間一定要結合業務實際情況設置,不能過長、也不能太短。
- 程序出異常時,結合
try{...}catch{...}
捕獲並釋放鎖,如果有必要業務需做回滾,補償動作,保證程序的健壯。