Redis 分散式鎖
分散式鎖的演變
- 本地鎖(單機用)
- 利用redis進行分散式鎖 使用 set
- 防止死鎖 加過期時間 使用 setnx
- 防止A請求未執行完 鎖過期刪除 B請求加鎖後 A完成後誤刪該鎖 使用 Hash結構, 規定每個請求只能刪除自己的鎖
- 保證併發安全,申請鎖和加過期時間需要 原子性,用 lua指令碼 加鎖或解鎖
- 考慮到 重入性 (每個請求只拿到一個鎖後,可以多函式或執行緒共用) 使用 Hash結構進行加減(hincrby) 操作
- 為了保證業務執行過長,鎖不會過期。需要對鎖進行 續期。
分散式鎖的特性
- 獨佔性
- 高可用
- 防死鎖
- 不亂搶
- 可重入
1. setNX
- 注意死鎖
- 注意lock過期時間和業務執行時間
- 加鎖不成功時,等待獲取機制 ( 注意停幾毫秒 )
- 一般情況下完全夠用
2. 考慮look重入性
- 當一個鎖被建立出來之後再同一個請求中不需要再額外申請其他鎖,一個鎖可以被重複使用,所以使用到 Hash資料結構的鎖
- 如果業務上需要應用到多個函式的鎖的情況下,申請一個鎖之後固定當前請求的uuid+當前執行緒id。入一個函式(或者執行緒)向Hash的uuid +1
- 直到請求結束後,檢查執行緒鎖是否歸0 如果是釋放該鎖,如果不是說明其他執行緒未能執行完畢。
- 考慮可重入性的遞減。 加鎖幾次就要減幾次,到0後刪除
鎖的操作保證原子性應對高併發
- 當一個鎖(Hash結構) 建立鎖和增加過期時間,兩步需要lua指令碼進行執行保證原子性。
// lua 指令碼編寫
// 加鎖操作 如果鎖不存在或者當前請求鎖的uuid欄位存在 則進行加一 (重入性)
KEYS[1] // -- 分散式鎖的key
ARGV[1] // -- 鎖的唯一標識,通常是執行緒ID或呼叫者標識
ARGV[2] // -- 鎖的過期時間,單位為毫秒
if redis.call('exists', KEYS[1]) == 0 //鎖不存在
or redis.call('hexists', KEYS[1], ARGV[1]) == 1 // 當前請求有鎖 進入函式(或新開執行緒)無需額外申請鎖
then
redis.call('hincrby',KEYS[1], ARGV[1], 1)
redis.call('expire',KEYS[1], ARGV[2])
return true
else
return false
end
//解鎖操作
if redis.call("hexists",KEYS[1], ARGV[1]) == 0 then // 無鎖 無需解鎖
return false
else if redis.call("hincrby",KEYS[1], ARGV[1], -1) == 0 then // 鎖存在且是自己請求的鎖 進行減一操作,如果減為0 則解鎖(刪key)
retrun redis.call("DEL",KEYS[1])
else
return 0
- 當lock 建立時,需要同步啟動一個定時續期任務,鎖存在並過該定時時間進行續期,防止業務未完直接釋放鎖。
- 當主執行緒執行完業務流程 並釋放鎖之後,續期機制同時結束。