Redis實(shí)現(xiàn)分布式鎖

Redis實(shí)現(xiàn)分布式鎖

一、Redis單節(jié)點(diǎn)實(shí)現(xiàn)

(一) 獲取鎖

使用 Redis 客戶端獲取鎖,向Redis發(fā)出下面的命令:

set key random_value NX PX 1000

上面的 SET 命令中:

  • random_value 是客戶端隨機(jī)產(chǎn)生的字符串,需要保證唯一性,用于防止誤刪鎖的情況

  • NX 表示 key 不存在的時(shí)候,SET 操作才成功,這樣保證了只有一個(gè)客戶端活動(dòng)鎖

  • PX 1000 表示這個(gè)鎖在 1000毫秒以后過期

(二) 鎖釋放

當(dāng)獲取鎖的客戶端完成了操作,需要執(zhí)行下面的命令釋放鎖:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

釋放鎖時(shí),要先比較 key 的 value 與 客戶端的 random_value 是否相等,如果相等,就是釋放鎖,否則失敗,這里的比較操作和刪除操作需要保證原子性,所以使用 lua 腳本實(shí)現(xiàn)

這里記錄一下在執(zhí)行 lua 腳本時(shí)遇到的坑

#錯(cuò)誤
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

上面兩行腳本唯一的不同點(diǎn)在于 ‘ 和 “, 在 redis-cli 執(zhí)行腳本的時(shí)候一定要注意

將執(zhí)行的 lua 腳本寫到文件中

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

執(zhí)行 lua 腳本文件

#錯(cuò)誤,逗號(hào)兩邊少了空格
redis-cli --eval del.lua name,heyong

#正確
redis-cli --eval del.lua name , heyong

調(diào)用lua 腳本的語法如下:

調(diào)用Lua腳本的語法:
$ redis-cli --eval path/to/redis.lua KEYS[1] KEYS[2] , ARGV[1] ARGV[2] 

KEYS[1],KEYS[2] : 代表要操作的鍵
ARGV[1] ARGV[2] : 參數(shù),在lua腳本中通過 ARGV[1] ARGV[2] 獲得

注意 : KEYS和ARGV中間的 ',' 兩邊的空格,不能省略

二、單機(jī)Redis分布式鎖相關(guān)問題

(一) 為什么需要設(shè)置過期時(shí)間

Redis實(shí)現(xiàn)分布式鎖必須要設(shè)置過期時(shí)間,否則某個(gè)客戶端獲取鎖以后,客戶端宕機(jī)獲取由于網(wǎng)絡(luò)問題無法與Redis節(jié)點(diǎn)通信,那么該客戶端就一直持有鎖,其他客戶端就無法獲得鎖。鎖的過期時(shí)間要根據(jù)自己的業(yè)務(wù)場景來,但是也不要設(shè)置的過長或者過短。

(二) 為什么鎖的獲取需要保證原子性

如果獲取鎖的操作是使用下面的命令會(huì)有說明問題

SETNX key random_value
EXPIRE key 10

在理想情況下,通過上面的命令也能夠獲得鎖,但是由于缺少原子操作,在執(zhí)行完第一條命令以后,客戶端重啟或者崩潰,那么鎖就一直沒有辦法釋放

(三) random_value的必要性

設(shè)置 random_value 保證了一個(gè)客戶端釋放的鎖必須是自己持有的鎖,如果不能保證random_value值的唯一性,就可能出現(xiàn)下面的情況:

  1. 客戶端A獲得鎖
  2. 客戶端在某個(gè)操作上阻塞
  3. key 過期,鎖自動(dòng)釋放
  4. 客戶端B獲得鎖
  5. 客戶端A完成業(yè)務(wù)操作,釋放掉客戶端B持有的鎖

由于客戶端B的鎖被釋放,那么就會(huì)有其他客戶端來獲取鎖,多個(gè)客戶端同時(shí)操作共享資源,導(dǎo)致臟數(shù)據(jù)

(四) 為什么鎖釋放需要保證原子性

在釋放鎖的時(shí)候, random_value是否等于key的value 和 key的刪除操作需要保證原子性,如果沒有保證原子性,就可能出現(xiàn)下面的情況:

  1. 客戶端A獲得鎖
  2. 客戶端A執(zhí)行業(yè)務(wù)操作
  3. 客戶端執(zhí)行GET命令獲取key的value,并與random_value相等
  4. 客戶端A發(fā)出del命令,但是由于網(wǎng)絡(luò)問題,導(dǎo)致請(qǐng)求時(shí)間過長
  5. key過期,鎖自動(dòng)釋放
  6. 客戶端B獲得鎖
  7. 客戶端A的請(qǐng)求到達(dá)Redis服務(wù)器,執(zhí)行del操作,客戶端B的鎖被釋放

客戶端B的鎖被釋放,就不能保護(hù)共享資源

(五) Redis主從對(duì)鎖的影響

在生產(chǎn)環(huán)境中,Redis部署至少會(huì)實(shí)現(xiàn)主從架構(gòu),并通過各種容災(zāi)機(jī)制,在主節(jié)點(diǎn)宕機(jī)的時(shí)候?qū)墓?jié)點(diǎn)升級(jí)為主節(jié)點(diǎn),在這種架構(gòu)下,分布式鎖也可能出現(xiàn)問題

  1. 客戶端A從Master節(jié)點(diǎn)獲取到鎖
  2. Master節(jié)點(diǎn)宕機(jī),并且key沒有及時(shí)同步到slave節(jié)點(diǎn)
  3. Slave節(jié)點(diǎn)升級(jí)為主節(jié)點(diǎn)
  4. 客戶端B獲得鎖

這樣也出現(xiàn)了共享資源被多個(gè)客戶端操作的情況

三、分布式鎖Redlock

上面提出提出了單機(jī)Redis分布式存在鎖安全的問題,于是Redis的作者Antirez提出的Redlock方案。
有關(guān)Redlock可以參考:
1、https://github.com/antirez/redis-doc/blob/master/topics/distlock.md
2、http://ifeve.com/redis-lock/

四、使用單機(jī)Redis分布式鎖還是Redlock

通過上面的分析,如果Redis主節(jié)點(diǎn)宕機(jī),可能會(huì)喪失鎖的安全性,但是是否項(xiàng)目使用單機(jī)Redis分布式鎖需要結(jié)合自己的業(yè)務(wù)場景考慮,下面舉一些場景作為參考

  1. 在網(wǎng)上商城中,商品的信息很少發(fā)生變化,所以會(huì)將商品數(shù)據(jù)緩存到Redis中,可能會(huì)出現(xiàn)并發(fā)請(qǐng)求一個(gè)熱點(diǎn)商品數(shù)據(jù)的情況,如果當(dāng)前熱點(diǎn)商品緩存過期,那么大量的請(qǐng)求就會(huì)打到 DB上,為了解決這種情況通常使用分布式鎖,讓獲取到鎖的線程去數(shù)據(jù)庫獲取商品數(shù)據(jù),在這里使用單機(jī)Redis實(shí)現(xiàn)分布式鎖,如果主節(jié)點(diǎn)宕機(jī)也不會(huì)影響數(shù)據(jù)的正確性,只是在短時(shí)間類可能出現(xiàn)多個(gè)請(qǐng)求打到DB,獲取相同的商品數(shù)據(jù)。

  2. 如果涉及到對(duì)某個(gè)共享資源的修改操作,需要保證數(shù)據(jù)的安全性,建議使用Redlock

五、擴(kuò)展

(一) SET 命令參數(shù)

在redis2.6以后,提供了相關(guān)的參數(shù)來設(shè)置 SET 命令的行為

  • EX second : 設(shè)置鍵的過期時(shí)間,過期時(shí)間以秒為單位
  • PX millisecond : 設(shè)置鍵的過期時(shí)間,過期時(shí)間以毫秒為單位
  • NX : 鍵不存在時(shí)才操作成功
  • XX : 鍵存在時(shí)才操作成功

(二) 其他分布式實(shí)現(xiàn)方案

  1. 基于數(shù)據(jù)庫實(shí)現(xiàn):在數(shù)據(jù)庫創(chuàng)建一張表,加鎖的機(jī)制就是在數(shù)據(jù)庫里面通過插入和刪除記錄實(shí)現(xiàn),當(dāng)需要加鎖的時(shí)候,創(chuàng)建一條數(shù)據(jù)庫記錄,釋放鎖的時(shí)候刪除記錄

  2. 基于Zookeeper實(shí)現(xiàn):ZK提供了臨時(shí)節(jié)點(diǎn),如果客戶端與ZK斷開連接,那么客戶端就會(huì)自動(dòng)刪除改臨時(shí)節(jié)點(diǎn)。同時(shí)ZK提供了 watcher 機(jī)制,如果節(jié)點(diǎn)發(fā)生了變更,會(huì)通知監(jiān)聽改節(jié)點(diǎn)的客戶端。

根據(jù)zookeeper的這些特性,我們來看看如何利用這些特性來實(shí)現(xiàn)分布式鎖:

  • 創(chuàng)建一個(gè)鎖目錄lock

  • 線程A獲取鎖會(huì)在lock目錄下,創(chuàng)建臨時(shí)順序節(jié)點(diǎn)

  • 獲取鎖目錄下所有的子節(jié)點(diǎn),然后獲取比自己小的兄弟節(jié)點(diǎn),如果不存在,則說明當(dāng)前線程順序號(hào)最小,獲得鎖

  • 線程B創(chuàng)建臨時(shí)節(jié)點(diǎn)并獲取所有兄弟節(jié)點(diǎn),判斷自己不是最小節(jié)點(diǎn),設(shè)置監(jiān)聽(watcher)比自己次小的節(jié)點(diǎn)(只關(guān)注比自己次小的節(jié)點(diǎn)是為了防止發(fā)生“羊群效應(yīng)”)

  • 線程A處理完,刪除自己的節(jié)點(diǎn),線程B監(jiān)聽到變更事件,判斷自己是最小的節(jié)點(diǎn),獲得鎖

上面的分布式鎖是基于ZK的臨時(shí)節(jié)點(diǎn)和watch機(jī)制實(shí)現(xiàn)的,該方案也存在問題,如果出現(xiàn)網(wǎng)絡(luò)抖動(dòng)問題,導(dǎo)致client和ZK集群斷開連接,那么臨時(shí)節(jié)點(diǎn)就會(huì)被自動(dòng)刪除,那么其他客戶端也可以獲取鎖。

可以使用 Apache 開源的curator 開實(shí)現(xiàn) Zookeeper 分布式鎖。

實(shí)現(xiàn)方式 優(yōu)點(diǎn) 缺點(diǎn) 使用場景
redis 性能高 實(shí)現(xiàn)復(fù)雜 安全性低 高并發(fā)的分布式鎖實(shí)現(xiàn)
zookeeper 有現(xiàn)成的框架,實(shí)現(xiàn)簡單, 同時(shí)ZK提供了臨時(shí)節(jié)點(diǎn)和watch機(jī)制,鎖的安全性相對(duì)較高 添加和刪除節(jié)點(diǎn)性能較低 并發(fā)量小,安全性要求較高的業(yè)務(wù)場景
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容