Redis實現分布式鎖
一、Redis單節點實現
(一) 獲取鎖
使用 Redis 客戶端獲取鎖,向Redis發出下面的命令:
set key random_value NX PX 1000
上面的 SET 命令中:
random_value 是客戶端隨機產生的字符串,需要保證唯一性,用于防止誤刪鎖的情況
NX 表示 key 不存在的時候,SET 操作才成功,這樣保證了只有一個客戶端活動鎖
PX 1000 表示這個鎖在 1000毫秒以后過期
(二) 鎖釋放
當獲取鎖的客戶端完成了操作,需要執行下面的命令釋放鎖:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
釋放鎖時,要先比較 key 的 value 與 客戶端的 random_value 是否相等,如果相等,就是釋放鎖,否則失敗,這里的比較操作和刪除操作需要保證原子性,所以使用 lua 腳本實現
這里記錄一下在執行 lua 腳本時遇到的坑
#錯誤
eval "if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end" 1 name heyong
#正確
eval 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end' 1 name heyong
上面兩行腳本唯一的不同點在于 ‘ 和 “, 在 redis-cli 執行腳本的時候一定要注意
將執行的 lua 腳本寫到文件中
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
執行 lua 腳本文件
#錯誤,逗號兩邊少了空格
redis-cli --eval del.lua name,heyong
#正確
redis-cli --eval del.lua name , heyong
調用lua 腳本的語法如下:
調用Lua腳本的語法:
$ redis-cli --eval path/to/redis.lua KEYS[1] KEYS[2] , ARGV[1] ARGV[2]
KEYS[1],KEYS[2] : 代表要操作的鍵
ARGV[1] ARGV[2] : 參數,在lua腳本中通過 ARGV[1] ARGV[2] 獲得
注意 : KEYS和ARGV中間的 ',' 兩邊的空格,不能省略
二、單機Redis分布式鎖相關問題
(一) 為什么需要設置過期時間
Redis實現分布式鎖必須要設置過期時間,否則某個客戶端獲取鎖以后,客戶端宕機獲取由于網絡問題無法與Redis節點通信,那么該客戶端就一直持有鎖,其他客戶端就無法獲得鎖。鎖的過期時間要根據自己的業務場景來,但是也不要設置的過長或者過短。
(二) 為什么鎖的獲取需要保證原子性
如果獲取鎖的操作是使用下面的命令會有說明問題
SETNX key random_value
EXPIRE key 10
在理想情況下,通過上面的命令也能夠獲得鎖,但是由于缺少原子操作,在執行完第一條命令以后,客戶端重啟或者崩潰,那么鎖就一直沒有辦法釋放
(三) random_value的必要性
設置 random_value 保證了一個客戶端釋放的鎖必須是自己持有的鎖,如果不能保證random_value值的唯一性,就可能出現下面的情況:
- 客戶端A獲得鎖
- 客戶端在某個操作上阻塞
- key 過期,鎖自動釋放
- 客戶端B獲得鎖
- 客戶端A完成業務操作,釋放掉客戶端B持有的鎖
由于客戶端B的鎖被釋放,那么就會有其他客戶端來獲取鎖,多個客戶端同時操作共享資源,導致臟數據
(四) 為什么鎖釋放需要保證原子性
在釋放鎖的時候, random_value是否等于key的value 和 key的刪除操作需要保證原子性,如果沒有保證原子性,就可能出現下面的情況:
- 客戶端A獲得鎖
- 客戶端A執行業務操作
- 客戶端執行GET命令獲取key的value,并與random_value相等
- 客戶端A發出del命令,但是由于網絡問題,導致請求時間過長
- key過期,鎖自動釋放
- 客戶端B獲得鎖
- 客戶端A的請求到達Redis服務器,執行del操作,客戶端B的鎖被釋放
客戶端B的鎖被釋放,就不能保護共享資源
(五) Redis主從對鎖的影響
在生產環境中,Redis部署至少會實現主從架構,并通過各種容災機制,在主節點宕機的時候將從節點升級為主節點,在這種架構下,分布式鎖也可能出現問題
- 客戶端A從Master節點獲取到鎖
- Master節點宕機,并且key沒有及時同步到slave節點
- Slave節點升級為主節點
- 客戶端B獲得鎖
這樣也出現了共享資源被多個客戶端操作的情況
三、分布式鎖Redlock
上面提出提出了單機Redis分布式存在鎖安全的問題,于是Redis的作者Antirez提出的Redlock方案。
有關Redlock可以參考:
1、https://github.com/antirez/redis-doc/blob/master/topics/distlock.md
2、http://ifeve.com/redis-lock/
四、使用單機Redis分布式鎖還是Redlock
通過上面的分析,如果Redis主節點宕機,可能會喪失鎖的安全性,但是是否項目使用單機Redis分布式鎖需要結合自己的業務場景考慮,下面舉一些場景作為參考
在網上商城中,商品的信息很少發生變化,所以會將商品數據緩存到Redis中,可能會出現并發請求一個熱點商品數據的情況,如果當前熱點商品緩存過期,那么大量的請求就會打到 DB上,為了解決這種情況通常使用分布式鎖,讓獲取到鎖的線程去數據庫獲取商品數據,在這里使用單機Redis實現分布式鎖,如果主節點宕機也不會影響數據的正確性,只是在短時間類可能出現多個請求打到DB,獲取相同的商品數據。
如果涉及到對某個共享資源的修改操作,需要保證數據的安全性,建議使用Redlock
五、擴展
(一) SET 命令參數
在redis2.6以后,提供了相關的參數來設置 SET 命令的行為
- EX second : 設置鍵的過期時間,過期時間以秒為單位
- PX millisecond : 設置鍵的過期時間,過期時間以毫秒為單位
- NX : 鍵不存在時才操作成功
- XX : 鍵存在時才操作成功
(二) 其他分布式實現方案
基于數據庫實現:在數據庫創建一張表,加鎖的機制就是在數據庫里面通過插入和刪除記錄實現,當需要加鎖的時候,創建一條數據庫記錄,釋放鎖的時候刪除記錄
基于Zookeeper實現:ZK提供了臨時節點,如果客戶端與ZK斷開連接,那么客戶端就會自動刪除改臨時節點。同時ZK提供了 watcher 機制,如果節點發生了變更,會通知監聽改節點的客戶端。
根據zookeeper的這些特性,我們來看看如何利用這些特性來實現分布式鎖:
創建一個鎖目錄lock
線程A獲取鎖會在lock目錄下,創建臨時順序節點
獲取鎖目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖
線程B創建臨時節點并獲取所有兄弟節點,判斷自己不是最小節點,設置監聽(watcher)比自己次小的節點(只關注比自己次小的節點是為了防止發生“羊群效應”)
線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是最小的節點,獲得鎖
上面的分布式鎖是基于ZK的臨時節點和watch機制實現的,該方案也存在問題,如果出現網絡抖動問題,導致client和ZK集群斷開連接,那么臨時節點就會被自動刪除,那么其他客戶端也可以獲取鎖。
可以使用 Apache 開源的curator 開實現 Zookeeper 分布式鎖。
實現方式 | 優點 | 缺點 | 使用場景 |
---|---|---|---|
redis | 性能高 | 實現復雜 安全性低 | 高并發的分布式鎖實現 |
zookeeper | 有現成的框架,實現簡單, 同時ZK提供了臨時節點和watch機制,鎖的安全性相對較高 | 添加和刪除節點性能較低 | 并發量小,安全性要求較高的業務場景 |