百度商橋 網(wǎng)站慢女兒考試沒(méi)圈關(guān)鍵詞
文章目錄
- 引言
- 一、Redis的兩種原子操作
- 1.1 Redis 的原子性
- 1.2 單命令
- 1.3 Lua 腳本
- 1.4 對(duì)比單命令與 Lua 腳本
- 二、Redis 實(shí)現(xiàn)分布式鎖
- 2.1 分布式鎖的概念與需求
- 2.1.1 什么是分布式鎖?
- 2.1.2 分布式鎖的常見(jiàn)應(yīng)用場(chǎng)景
- 2.2 基于 Redis 的分布式鎖實(shí)現(xiàn)
- 2.2.1 鎖的獲取與釋放
- 2.2.2 獲取鎖的實(shí)現(xiàn)
- 2.2.3 釋放鎖的實(shí)現(xiàn)
- 2.3 失效機(jī)制與超時(shí)設(shè)置
- 2.3.1 為什么需要超時(shí)機(jī)制?
- 2.3.2 使用 Redis 過(guò)期時(shí)間
- 2.3.3 鎖續(xù)約
- 2.4 RedLock 算法
- 2.4.1 RedLock 的工作流程
- 2.4.2 RedLock 的優(yōu)缺點(diǎn)
- 三、分布式鎖的優(yōu)缺點(diǎn)與應(yīng)用場(chǎng)景
- 3.1 Redis 分布式鎖的優(yōu)點(diǎn)
- 3.2 Redis 分布式鎖的缺點(diǎn)
- 3.3 分布式鎖的典型應(yīng)用場(chǎng)景
- 3.3.1 單點(diǎn)任務(wù)執(zhí)行
- 3.3.2 秒殺場(chǎng)景的庫(kù)存控制
- 3.4 Redis 分布式鎖與其他實(shí)現(xiàn)方式的對(duì)比
- 四、Redis 事務(wù)
- 4.1 Redis 事務(wù)回滾
- 4.2 Redis 事務(wù)的行為
- 4.4 Redis 為什么不支持回滾?
- 4.5 Redis 事務(wù)與傳統(tǒng)事務(wù)的對(duì)比
- 五、Lua 腳本 vs Redis 事務(wù)
- 5.1 Lua 腳本天然支持原子性
- 5.2 Redis 事務(wù)的局限性
- 5.3 Lua 腳本更加靈活
- 六、對(duì)比與總結(jié)
- 6.1 Redis 分布式鎖與其他鎖實(shí)現(xiàn)方式的對(duì)比
- 6.1.1 基于 Redis 的分布式鎖
- 6.1.2 基于數(shù)據(jù)庫(kù)的分布式鎖
- 6.1.3 基于 Zookeeper 的分布式鎖
- 6.1.4 對(duì)比總結(jié)
- 6.2 實(shí)踐中的最佳建議
- 6.3 Redis 分布式鎖的應(yīng)用建議
- 6.4 總結(jié)
引言
在現(xiàn)代分布式系統(tǒng)中,分布式鎖是一種核心的技術(shù)手段,能夠保證在多個(gè)節(jié)點(diǎn)或進(jìn)程中對(duì)共享資源的安全訪(fǎng)問(wèn)。它通過(guò)提供互斥機(jī)制,確保在同一時(shí)刻只有一個(gè)客戶(hù)端能夠操作關(guān)鍵資源。這種能力對(duì)于處理高并發(fā)請(qǐng)求和避免資源爭(zhēng)奪至關(guān)重要。
隨著微服務(wù)架構(gòu)的廣泛應(yīng)用,分布式鎖的需求變得更加迫切。例如,在訂單系統(tǒng)中,多個(gè)實(shí)例可能同時(shí)嘗試更新同一庫(kù)存數(shù)據(jù);在任務(wù)調(diào)度中,確保定時(shí)任務(wù)不被多個(gè)實(shí)例重復(fù)執(zhí)行是必要的。分布式鎖不僅是技術(shù)實(shí)現(xiàn)中的關(guān)鍵模塊,也在業(yè)務(wù)邏輯中扮演著重要角色。
Redis 作為高性能的內(nèi)存數(shù)據(jù)庫(kù),以其簡(jiǎn)單易用、快速響應(yīng)的特點(diǎn),成為實(shí)現(xiàn)分布式鎖的常用選擇。Redis 提供了豐富的原子操作,如 SETNX
和 Lua 腳本,為分布式鎖的實(shí)現(xiàn)提供了堅(jiān)實(shí)的基礎(chǔ)。此外,Redis 的 RedLock 算法更進(jìn)一步解決了單點(diǎn)故障的問(wèn)題,為高可靠性需求的系統(tǒng)提供了有力支持。
然而,在實(shí)際場(chǎng)景中,Redis 分布式鎖也面臨一些挑戰(zhàn),如如何設(shè)計(jì)鎖的超時(shí)時(shí)間、如何防止鎖誤刪,以及如何在高可靠性場(chǎng)景下確保鎖的有效性。本篇文章將深入探討 Redis 分布式鎖的原理與實(shí)現(xiàn),結(jié)合具體的 Go 語(yǔ)言示例代碼,逐步分析如何在分布式系統(tǒng)中高效且可靠地使用 Redis 分布式鎖。
一、Redis的兩種原子操作
1.1 Redis 的原子性
Redis 的所有命令天生具備原子性。這意味著每條命令在 Redis 服務(wù)器中要么完全執(zhí)行,要么完全失敗,絕不會(huì)中途打斷。這一特性為并發(fā)控制提供了可靠的基礎(chǔ)。
示例
考慮如下場(chǎng)景:在高并發(fā)環(huán)境下,多個(gè)客戶(hù)端嘗試同時(shí)獲取同一分布式鎖。假設(shè)只有一個(gè)客戶(hù)端能夠成功獲取鎖,其余客戶(hù)端必須排隊(duì)等待或直接失敗返回。Redis 的原子操作能夠確保這種互斥性。
1.2 單命令
支持原子操作的常用命令
-
INCR
和DECR
:原子遞增/遞減
這些命令常用于計(jì)數(shù)器場(chǎng)景,通過(guò)單條命令完成值的增加或減少操作。示例
INCR counter_key DECR counter_key
應(yīng)用場(chǎng)景
適用于請(qǐng)求限流或資源配額管理場(chǎng)景。例如:- 用戶(hù)訪(fǎng)問(wèn)次數(shù)計(jì)數(shù)。
- 秒殺商品的庫(kù)存控制。
-
SET
命令
SET
是 Redis 最靈活的原子操作命令之一,支持多個(gè)選項(xiàng),能夠同時(shí)完成鍵值設(shè)置和過(guò)期時(shí)間的配置。示例
SET lock_key "value" NX EX 30
NX
:僅在鍵不存在時(shí)設(shè)置值。EX 30
:設(shè)置鍵的過(guò)期時(shí)間為 30 秒。
優(yōu)點(diǎn)
- 使用單條命令即可完成鎖的獲取與自動(dòng)失效。
- 高效、簡(jiǎn)單,避免了競(jìng)爭(zhēng)條件。
-
SETNX
命令
SETNX(SET if Not Exists)
是 Redis 專(zhuān)為互斥性操作設(shè)計(jì)的命令,用于“僅在鍵不存在時(shí)設(shè)置值”。然而,SETNX
本身不支持直接設(shè)置過(guò)期時(shí)間,常需要與EXPIRE
組合使用,這可能導(dǎo)致非原子性問(wèn)題。示例
SETNX lock_key "value" EXPIRE lock_key 30
問(wèn)題
如果在SETNX
成功執(zhí)行后,EXPIRE
執(zhí)行前發(fā)生宕機(jī),會(huì)導(dǎo)致鎖沒(méi)有設(shè)置過(guò)期時(shí)間,從而引發(fā)死鎖問(wèn)題。優(yōu)化建議
在現(xiàn)代 Redis 應(yīng)用中,推薦使用SET
命令代替SETNX
,通過(guò)選項(xiàng)直接設(shè)置過(guò)期時(shí)間。
SET
和 SETNX
的對(duì)比
特性 | SET | SETNX |
---|---|---|
功能 | 設(shè)置值,并支持過(guò)期時(shí)間 | 僅在鍵不存在時(shí)設(shè)置值 |
靈活性 | 高 | 低 |
常見(jiàn)使用場(chǎng)景 | 分布式鎖、緩存鍵設(shè)置 | 簡(jiǎn)單的互斥操作 |
推薦程度 | ★★★★★ | ★★ |
結(jié)論
在實(shí)現(xiàn)分布式鎖時(shí),SET
是更優(yōu)的選擇,能夠簡(jiǎn)化邏輯并避免競(jìng)爭(zhēng)條件。
1.3 Lua 腳本
Lua 腳本的功能與優(yōu)勢(shì)
Lua 腳本允許開(kāi)發(fā)者將多個(gè) Redis 命令組合成一個(gè)原子操作。這種組合方式特別適合復(fù)雜的并發(fā)場(chǎng)景。
示例:通過(guò) Lua 腳本實(shí)現(xiàn)鎖的獲取和過(guò)期時(shí)間設(shè)置
EVAL "if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then return redis.call('EXPIRE', KEYS[1], ARGV[2])
else return 0 end" 1 lock_key "value" 30
腳本邏輯
- 檢查鍵是否存在。
- 如果鍵不存在,設(shè)置值并添加過(guò)期時(shí)間。
- 返回操作結(jié)果。
優(yōu)點(diǎn)
- 保證多步驟操作的原子性。
- 避免傳統(tǒng)組合命令可能引發(fā)的競(jìng)爭(zhēng)條件。
Lua 腳本的性能
Redis 將 Lua 腳本加載到內(nèi)存中執(zhí)行,性能非常高。腳本執(zhí)行過(guò)程中不會(huì)被其他命令打斷,確保操作的完整性。
應(yīng)用場(chǎng)景
- 分布式鎖的獲取與釋放。
- 批量處理復(fù)雜數(shù)據(jù)操作。
1.4 對(duì)比單命令與 Lua 腳本
使用單命令和 Lua 腳本實(shí)現(xiàn)分布式鎖的獲取和釋放:
單命令
SET lock_key "value" NX EX 30
Lua 腳本
EVAL "if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then return redis.call('EXPIRE', KEYS[1], ARGV[2]) else return 0 end" 1 lock_key "value" 30
特性 | 單命令 (SET) | Lua 腳本 |
---|---|---|
簡(jiǎn)單性 | ★★★★★ | ★★★ |
靈活性 | ★★★★ | ★★★★★ |
性能 | ★★★★★ | ★★★★ |
場(chǎng)景適配性 | 常見(jiàn)鎖場(chǎng)景 | 自定義復(fù)雜邏輯 |
分布式鎖的獲取邏輯
二、Redis 實(shí)現(xiàn)分布式鎖
分布式鎖在分布式系統(tǒng)中至關(guān)重要,特別是在多服務(wù)或多實(shí)例環(huán)境下,確保同一時(shí)間只有一個(gè)客戶(hù)端能夠訪(fǎng)問(wèn)關(guān)鍵資源。Redis 提供了高效實(shí)現(xiàn)分布式鎖的能力,但正確使用這些能力仍需充分理解其工作原理和潛在問(wèn)題。
2.1 分布式鎖的概念與需求
2.1.1 什么是分布式鎖?
分布式鎖是一種用于分布式環(huán)境中同步共享資源訪(fǎng)問(wèn)的機(jī)制,它能夠確保多進(jìn)程、多節(jié)點(diǎn)環(huán)境中的互斥性。
分布式鎖需要滿(mǎn)足的核心特性:
- 互斥性:任意時(shí)刻,只有一個(gè)客戶(hù)端可以持有鎖。
- 無(wú)死鎖:即使持鎖的客戶(hù)端出現(xiàn)故障,鎖也能在合理時(shí)間后自動(dòng)釋放。
- 容錯(cuò)性:即使 Redis 實(shí)例發(fā)生部分故障,鎖仍然可用。
- 高性能:獲取和釋放鎖的操作必須快速,適合高并發(fā)場(chǎng)景。
2.1.2 分布式鎖的常見(jiàn)應(yīng)用場(chǎng)景
- 單點(diǎn)任務(wù)執(zhí)行:確保任務(wù)僅由一個(gè)節(jié)點(diǎn)執(zhí)行,例如數(shù)據(jù)庫(kù)遷移。
- 限流控制:防止超出預(yù)期的流量,保護(hù)后端服務(wù)。
- 資源競(jìng)爭(zhēng):在電商秒殺或搶購(gòu)活動(dòng)中,確保同一商品不被超賣(mài)。
- 跨服務(wù)事務(wù):協(xié)調(diào)多個(gè)微服務(wù)共同完成一個(gè)分布式事務(wù)。
案例:秒殺商品庫(kù)存控制
在秒殺活動(dòng)中,每次下單操作必須驗(yàn)證庫(kù)存,并減少相應(yīng)數(shù)量。這需要分布式鎖來(lái)確保多個(gè)客戶(hù)端不會(huì)同時(shí)操作庫(kù)存,導(dǎo)致超賣(mài)。
2.2 基于 Redis 的分布式鎖實(shí)現(xiàn)
2.2.1 鎖的獲取與釋放
使用 Redis 實(shí)現(xiàn)分布式鎖的基本過(guò)程包括兩步:
- 獲取鎖:嘗試在 Redis 中設(shè)置一個(gè)唯一鍵,成功即表示鎖定資源。
- 釋放鎖:僅持有鎖的客戶(hù)端可以釋放鎖,以防止誤操作。
2.2.2 獲取鎖的實(shí)現(xiàn)
-
基本實(shí)現(xiàn)
使用SET
命令獲取鎖:SET lock_key value NX EX 30
NX
:保證僅當(dāng)鍵不存在時(shí)設(shè)置值,避免覆蓋現(xiàn)有鎖。EX 30
:設(shè)置鍵的過(guò)期時(shí)間為 30 秒,確保鎖能夠自動(dòng)失效。
-
流程圖:獲取鎖邏輯
-
優(yōu)化實(shí)現(xiàn):增加唯一標(biāo)識(shí)
為鎖的值添加一個(gè)唯一標(biāo)識(shí)(如 UUID),確保只有持有該唯一標(biāo)識(shí)的客戶(hù)端能釋放鎖。
示例SET lock_key "uuid-12345" NX EX 30
2.2.3 釋放鎖的實(shí)現(xiàn)
釋放鎖時(shí)必須確保是鎖的持有者操作,避免誤刪其他客戶(hù)端持有的鎖。
- 基本實(shí)現(xiàn)
使用DEL
命令釋放鎖:DEL lock_key
問(wèn)題:如果不檢查鎖的持有者身份,可能導(dǎo)致誤刪。
舉例:如果客戶(hù)端 A 執(zhí)行了 SET 命令加鎖后,假設(shè)客戶(hù)端 B 執(zhí)行了 DEL 命令釋放鎖,此時(shí),客戶(hù)端 A 的鎖就被誤釋放了。如果客戶(hù)端 C 正好也在申請(qǐng)加鎖, 就可以成功獲得鎖,進(jìn)而開(kāi)始操作共享數(shù)據(jù)。這樣一來(lái),客戶(hù)端 A 和 C 同時(shí)在對(duì)共享數(shù)據(jù) 進(jìn)行操作,數(shù)據(jù)就會(huì)被修改錯(cuò)誤,這也是業(yè)務(wù)層不能接受的。
為了應(yīng)對(duì)這個(gè)問(wèn)題,我們需要能區(qū)分來(lái)自不同客戶(hù)端的鎖操作,具體咋做呢?其實(shí),我們 可以在鎖變量的值上想想辦法。
-
改進(jìn)實(shí)現(xiàn):檢查鎖的持有者
使用 Lua 腳本確保原子性:EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock_key "uuid-12345"
- 邏輯:
- 獲取鍵的值,驗(yàn)證持有者身份。
- 如果身份匹配,則刪除鍵。
- 返回操作結(jié)果。
- 邏輯:
-
流程圖:釋放鎖邏輯
2.3 失效機(jī)制與超時(shí)設(shè)置
2.3.1 為什么需要超時(shí)機(jī)制?
在分布式環(huán)境中,客戶(hù)端可能因意外(如網(wǎng)絡(luò)故障、程序崩潰)失去鎖的控制權(quán)。超時(shí)機(jī)制可以防止鎖無(wú)限期存在,導(dǎo)致資源被長(zhǎng)期占用。
2.3.2 使用 Redis 過(guò)期時(shí)間
在鎖的獲取時(shí)設(shè)置過(guò)期時(shí)間是最簡(jiǎn)單的失效機(jī)制:
SET lock_key "value" NX EX 30
關(guān)鍵點(diǎn)
- 過(guò)期時(shí)間必須合理設(shè)置,避免鎖在任務(wù)未完成時(shí)被釋放。
- 在任務(wù)可能超過(guò)預(yù)期時(shí)間的情況下,需要考慮鎖續(xù)約機(jī)制。
2.3.3 鎖續(xù)約
為了確保任務(wù)能在復(fù)雜場(chǎng)景下順利完成,可能需要續(xù)約鎖的過(guò)期時(shí)間。
實(shí)現(xiàn)方法:
- 使用定時(shí)任務(wù)定期續(xù)約鎖。
- 檢查鎖的持有者身份后,延長(zhǎng)過(guò)期時(shí)間。
示例:續(xù)約腳本
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('EXPIRE', KEYS[1], ARGV[2]) else return 0 end" 1 lock_key "uuid-12345" 30
2.4 RedLock 算法
上面我們已經(jīng)了解了如何使用 SET 命令和 Lua 腳本在 Redis 單節(jié)點(diǎn)上實(shí)現(xiàn)分布式鎖。但是,我們現(xiàn)在只用了一個(gè) Redis 實(shí)例來(lái)保存鎖變量,如果這個(gè) Redis 實(shí)例發(fā)生故障宕機(jī)了,那么鎖變量就沒(méi)有了。此時(shí),客戶(hù)端也無(wú)法進(jìn)行鎖操作了,這就會(huì)影響到業(yè)務(wù)的正常執(zhí)行。所以,我們?cè)趯?shí)現(xiàn)分布式鎖時(shí),還需要保證鎖的可靠性。
為了避免 Redis 實(shí)例故障而導(dǎo)致的鎖無(wú)法工作的問(wèn)題,Redis 官方提出了分布式鎖算法Redlock,用于提高鎖的可靠性。
它的核心思想是通過(guò)多個(gè)獨(dú)立的 Redis 實(shí)例實(shí)現(xiàn)鎖的容錯(cuò)。
2.4.1 RedLock 的工作流程
- 客戶(hù)端嘗試在所有實(shí)例上獲取鎖。
- 如果在大多數(shù)實(shí)例上獲取鎖成功,并且總時(shí)間小于鎖的有效期,鎖定成功。
- 如果失敗,釋放已獲取的鎖并重試。
示意圖
2.4.2 RedLock 的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 容錯(cuò)性高,即使部分 Redis 實(shí)例故障,鎖仍然有效。
- 提供更高的可靠性,適合多數(shù)據(jù)中心部署。
缺點(diǎn)
- 實(shí)現(xiàn)復(fù)雜度較高,適合對(duì)可靠性要求極高的場(chǎng)景。
- 對(duì)網(wǎng)絡(luò)延遲敏感。
總結(jié)
- Redis 提供了簡(jiǎn)單高效的分布式鎖實(shí)現(xiàn)方法,但需要合理處理鎖的過(guò)期和釋放機(jī)制。
- 對(duì)于高可靠性需求場(chǎng)景,可以考慮使用 RedLock 算法。
三、分布式鎖的優(yōu)缺點(diǎn)與應(yīng)用場(chǎng)景
Redis 提供了靈活高效的分布式鎖解決方案,但在實(shí)際使用中,仍需要權(quán)衡其優(yōu)缺點(diǎn),以滿(mǎn)足具體業(yè)務(wù)需求。在本章中,我們將深入探討 Redis 分布式鎖的優(yōu)勢(shì)和不足,并通過(guò) Go 語(yǔ)言實(shí)現(xiàn)具體業(yè)務(wù)場(chǎng)景的代碼示例。
3.1 Redis 分布式鎖的優(yōu)點(diǎn)
-
高性能
- Redis 是內(nèi)存數(shù)據(jù)庫(kù),讀寫(xiě)速度極快,能夠支持高并發(fā)環(huán)境。
- 單命令(如
SET
)和 Lua 腳本的原子性保證鎖操作的高效性。
-
簡(jiǎn)單易用
- Redis 提供的鎖機(jī)制易于實(shí)現(xiàn),僅需幾行代碼即可完成鎖的獲取與釋放。
- 通過(guò)
SET
命令或 Lua 腳本可以實(shí)現(xiàn)大多數(shù)鎖的需求。
-
靈活性
- 支持多種方式實(shí)現(xiàn)分布式鎖:單命令、Lua 腳本、RedLock 算法。
- 可通過(guò)多實(shí)例部署提升可靠性。
3.2 Redis 分布式鎖的缺點(diǎn)
-
單點(diǎn)故障
- 如果 Redis 部署為單節(jié)點(diǎn)實(shí)例,當(dāng)節(jié)點(diǎn)故障時(shí),鎖可能失效。
- 解決方法:使用 Redis 集群或 RedLock 算法。
-
網(wǎng)絡(luò)延遲與時(shí)鐘漂移
- 鎖的過(guò)期時(shí)間依賴(lài)于客戶(hù)端與 Redis 之間的通信延遲。
- 如果網(wǎng)絡(luò)異?;驎r(shí)鐘漂移嚴(yán)重,可能導(dǎo)致鎖過(guò)早或過(guò)晚失效。
-
誤刪鎖的風(fēng)險(xiǎn)
- 如果鎖的釋放操作未驗(yàn)證持有者身份,可能誤刪其他客戶(hù)端的鎖。
- 解決方法:使用帶唯一標(biāo)識(shí)的值結(jié)合 Lua 腳本。
-
一致性問(wèn)題
- RedLock 算法在部分實(shí)例故障時(shí),可能無(wú)法滿(mǎn)足嚴(yán)格一致性需求。
3.3 分布式鎖的典型應(yīng)用場(chǎng)景
3.3.1 單點(diǎn)任務(wù)執(zhí)行
業(yè)務(wù)需求
確保同一時(shí)刻只有一個(gè)任務(wù)在某服務(wù)實(shí)例中執(zhí)行。例如:生成每日?qǐng)?bào)告任務(wù)。
Go 語(yǔ)言實(shí)現(xiàn)
package mainimport ("context""fmt""github.com/go-redis/redis/v8""time"
)var ctx = context.Background()func acquireLock(client *redis.Client, lockKey string, value string, expiration time.Duration) bool {// 嘗試獲取鎖ok, err := client.SetNX(ctx, lockKey, value, expiration).Result()if err != nil {fmt.Println("Error acquiring lock:", err)return false}return ok
}func releaseLock(client *redis.Client, lockKey string, value string) bool {// 使用 Lua 腳本釋放鎖script := `if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])elsereturn 0end`result, err := client.Eval(ctx, script, []string{lockKey}, value).Result()if err != nil {fmt.Println("Error releasing lock:", err)return false}return result.(int64) == 1
}func main() {client := redis.NewClient(&redis.Options{Addr: "localhost:6379",})lockKey := "daily_task_lock"lockValue := "unique-id-12345"lockExpiration := 30 * time.Secondif acquireLock(client, lockKey, lockValue, lockExpiration) {fmt.Println("Lock acquired. Executing task...")// 模擬任務(wù)執(zhí)行time.Sleep(10 * time.Second)// 釋放鎖if releaseLock(client, lockKey, lockValue) {fmt.Println("Lock released.")} else {fmt.Println("Failed to release lock.")}} else {fmt.Println("Failed to acquire lock. Another instance might be running the task.")}
}
運(yùn)行結(jié)果
- 如果鎖獲取成功,輸出
Lock acquired. Executing task...
,并在完成任務(wù)后釋放鎖。 - 如果鎖獲取失敗,輸出
Failed to acquire lock. Another instance might be running the task.
3.3.2 秒殺場(chǎng)景的庫(kù)存控制
業(yè)務(wù)需求
在秒殺場(chǎng)景中,需要確保同一時(shí)刻只有一個(gè)客戶(hù)端能夠成功扣減庫(kù)存,避免超賣(mài)。
Go 語(yǔ)言實(shí)現(xiàn)
var decStockLua = ` -- 參數(shù)local skey = KEYS[1] -- 庫(kù)存鍵local decrement = tonumber(ARGV[1]) -- 要扣減的數(shù)量,轉(zhuǎn)換為數(shù)字-- 判斷 key 是否存在if redis.call('EXISTS', skey) == 0 thenreturn {err = "Key does not exist"}end-- 獲取庫(kù)存值并轉(zhuǎn)換為數(shù)字local stock = tonumber(redis.call('GET', skey))-- 判斷庫(kù)存是否充足if stock >= decrement then-- 扣減庫(kù)存redis.call('DECRBY', skey, decrement)return {ok = "Decrement successful"}else-- 扣減失敗return {err = "Insufficient stock"}end`func main() {client := redis.NewClient(&redis.Options{Addr: "localhost:6379",})lockKey := "product_lock"num := 3var ctx = context.Background()client.SetNX(ctx, lockKey, 100, time.Hour) // 初始化庫(kù)存為100result, err := client.Eval(ctx, decStockLua, []string{lockKey}, num).Result()if err != nil {fmt.Println("Error :", err)return}if result.(string) == "Decrement successful" {fmt.Println("Successful")} else {fmt.Println("Error")}return
}
運(yùn)行結(jié)果
- 如果庫(kù)存充足,返回
Decrement successful
,同時(shí)扣減庫(kù)存。 - 如果庫(kù)存不足,輸出
Insufficient stock
。 - 如果庫(kù)存沒(méi)有初始化,輸出
Key does not exist
。
3.4 Redis 分布式鎖與其他實(shí)現(xiàn)方式的對(duì)比
實(shí)現(xiàn)方式 | 優(yōu)點(diǎn) | 缺點(diǎn) | 場(chǎng)景適配性 |
---|---|---|---|
Redis 分布式鎖 | 高性能、易用、靈活 | 單點(diǎn)故障風(fēng)險(xiǎn),誤刪風(fēng)險(xiǎn) | 高并發(fā)、低延遲場(chǎng)景 |
數(shù)據(jù)庫(kù)分布式鎖 | 一致性強(qiáng) | 性能較差,復(fù)雜度高 | 高一致性需求場(chǎng)景 |
Zookeeper 分布式鎖 | 高可靠性,支持會(huì)話(huà)失效 | 部署復(fù)雜,性能不如 Redis | 跨節(jié)點(diǎn)、容錯(cuò)場(chǎng)景 |
結(jié)論
Redis 分布式鎖憑借高效和靈活的特點(diǎn),在許多高并發(fā)場(chǎng)景中表現(xiàn)優(yōu)異。然而,根據(jù)具體業(yè)務(wù)需求選擇合適的實(shí)現(xiàn)方式,才是設(shè)計(jì)高質(zhì)量分布式系統(tǒng)的關(guān)鍵。對(duì)于具有高一致性要求的系統(tǒng),可以考慮 Zookeeper 或數(shù)據(jù)庫(kù)鎖;而對(duì)于性能敏感的系統(tǒng),Redis 是不二之選。
問(wèn)題:MySQL是通過(guò)事務(wù)來(lái)保證原子性的,Redis也是支持事務(wù)的,那為什么Redis不使用事務(wù)來(lái)保證原子性呢?
四、Redis 事務(wù)
在回答上文的問(wèn)題之前,先了解Redis事務(wù),只有了解了Redis事務(wù),才能更好地理解為什么Redis不使用事務(wù)來(lái)保證原子性。
4.1 Redis 事務(wù)回滾
在傳統(tǒng)數(shù)據(jù)庫(kù)中,事務(wù)的回滾是指在事務(wù)執(zhí)行過(guò)程中,如果出現(xiàn)錯(cuò)誤或未滿(mǎn)足某些條件,可以撤銷(xiāo)事務(wù)中已經(jīng)成功執(zhí)行的操作,將數(shù)據(jù)狀態(tài)恢復(fù)到事務(wù)開(kāi)始時(shí)的狀態(tài)。
關(guān)鍵點(diǎn):
- 回滾范圍:包括事務(wù)中已經(jīng)成功執(zhí)行的操作。
- 保障一致性:事務(wù)失敗時(shí),數(shù)據(jù)完全恢復(fù),不留下任何副作用。
Redis 的 DISCARD
命令用于在 EXEC
之前放棄事務(wù)隊(duì)列。
DISCARD
的使用
MULTI
SET key1 value1
INCR key2
DISCARD
MULTI
開(kāi)啟事務(wù)。SET key1 value1
和INCR key2
被放入隊(duì)列。- 執(zhí)行
DISCARD
時(shí),隊(duì)列被清空,事務(wù)未提交,任何命令都不會(huì)執(zhí)行。
與回滾的區(qū)別
DISCARD
僅用于放棄事務(wù)隊(duì)列,完全不會(huì)執(zhí)行任何操作。它無(wú)法撤銷(xiāo)已經(jīng)執(zhí)行的命令,因?yàn)?Redis 的事務(wù)命令只有在 EXEC
后才會(huì)實(shí)際執(zhí)行。
4.2 Redis 事務(wù)的行為
Redis 的事務(wù)通過(guò) MULTI
和 EXEC
命令定義,但它的機(jī)制與傳統(tǒng)數(shù)據(jù)庫(kù)不同。Redis 的事務(wù)是 命令隊(duì)列化執(zhí)行:
MULTI
:開(kāi)啟事務(wù),之后的命令被放入隊(duì)列。EXEC
:提交事務(wù),執(zhí)行隊(duì)列中的所有命令。- 事務(wù)執(zhí)行過(guò)程:
- 隊(duì)列中的所有命令會(huì)按順序一次性執(zhí)行。
- 單個(gè)命令是原子的,但 Redis 不支持事務(wù)整體的原子性。
示例:Redis 事務(wù)
MULTI
SET key1 value1
INCR key2
EXEC
SET key1 value1
會(huì)加入隊(duì)列。INCR key2
也會(huì)加入隊(duì)列。- 執(zhí)行
EXEC
時(shí),這些命令會(huì)按順序執(zhí)行。
如果事務(wù)中的某條命令失敗?
- Redis 的事務(wù)不會(huì)停止或回滾。
- 失敗的命令會(huì)返回錯(cuò)誤,其他命令繼續(xù)執(zhí)行。
示例:
MULTI
SET key1 value1
INCR key2 ## 如果 key2 不是整數(shù),這里會(huì)報(bào)錯(cuò)
SET key3 value3
EXEC
結(jié)果:
SET key1 value1
成功。INCR key2
失敗(如果key2
不是整數(shù))。SET key3 value3
成功。- 事務(wù)不會(huì)自動(dòng)回滾,
SET key1
和SET key3
的結(jié)果會(huì)保留。
4.4 Redis 為什么不支持回滾?
Redis 的設(shè)計(jì)哲學(xué)決定了它不支持回滾機(jī)制,原因如下:
1. 性能優(yōu)先
- 回滾機(jī)制需要在事務(wù)開(kāi)始前記錄所有被修改數(shù)據(jù)的快照(如數(shù)據(jù)庫(kù)中的 Undo Log)。
- 這會(huì)顯著增加內(nèi)存開(kāi)銷(xiāo),并降低 Redis 的寫(xiě)入性能。
2. 數(shù)據(jù)模型的簡(jiǎn)單性
- Redis 是一個(gè)高性能的內(nèi)存數(shù)據(jù)庫(kù),其設(shè)計(jì)目標(biāo)是簡(jiǎn)單高效。
- 引入回滾機(jī)制會(huì)增加 Redis 的實(shí)現(xiàn)復(fù)雜度,與其設(shè)計(jì)理念不符。
3. 操作原子性
- Redis 保證每條命令的原子性,這在大多數(shù)使用場(chǎng)景中已經(jīng)足夠。
- 對(duì)于需要更復(fù)雜事務(wù)機(jī)制的場(chǎng)景,通常會(huì)選擇其他工具(如傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù))。
4.5 Redis 事務(wù)與傳統(tǒng)事務(wù)的對(duì)比
特性 | Redis 事務(wù) | 傳統(tǒng)數(shù)據(jù)庫(kù)事務(wù) |
---|---|---|
回滾支持 | 不支持 | 支持 |
錯(cuò)誤處理 | 單命令錯(cuò)誤不會(huì)中止 | 自動(dòng)回滾或重試 |
原子性范圍 | 單命令 | 整體事務(wù) |
性能 | 高 | 較低 |
復(fù)雜性 | 低 | 高 |
五、Lua 腳本 vs Redis 事務(wù)
再來(lái)對(duì)比一下 Lua 腳本和 Redis 事務(wù),看看它們?cè)诒WC原子性方面的差異。
5.1 Lua 腳本天然支持原子性
Lua 腳本在 Redis 內(nèi)部執(zhí)行時(shí)是完全原子的:
- 當(dāng) Lua 腳本運(yùn)行時(shí),Redis 不會(huì)處理其他命令。
- 整個(gè)腳本的執(zhí)行要么全部成功,要么全部失敗。
這一點(diǎn)非常類(lèi)似于事務(wù)的概念,但實(shí)現(xiàn)方式更簡(jiǎn)單,且性能更高。Lua 腳本將邏輯和操作打包為一個(gè)命令發(fā)送到 Redis,因此避免了事務(wù)中潛在的競(jìng)爭(zhēng)條件。
5.2 Redis 事務(wù)的局限性
Redis 的事務(wù)通過(guò) MULTI
和 EXEC
命令實(shí)現(xiàn),但它的機(jī)制并不像傳統(tǒng)數(shù)據(jù)庫(kù)中的事務(wù)那樣強(qiáng)大,主要體現(xiàn)在以下幾個(gè)方面:
-
沒(méi)有回滾機(jī)制
Redis 事務(wù)不支持回滾。如果事務(wù)中的某條命令執(zhí)行失敗,其余命令仍會(huì)繼續(xù)執(zhí)行。
示例:MULTI SET key1 value1 INCR key2 # 如果 key2 不是數(shù)字,這里會(huì)報(bào)錯(cuò) EXEC
在上面的例子中,即使
INCR key2
出錯(cuò),SET key1 value1
仍然會(huì)被執(zhí)行。對(duì)于需要強(qiáng)一致性的場(chǎng)景,這可能會(huì)引發(fā)問(wèn)題。 -
不支持條件判斷
Redis 事務(wù)中無(wú)法直接進(jìn)行條件判斷。例如,要實(shí)現(xiàn)“如果某個(gè)鍵的值滿(mǎn)足條件,則執(zhí)行某個(gè)操作”,需要依賴(lài)客戶(hù)端邏輯,而 Lua 腳本可以直接在腳本中實(shí)現(xiàn)。 -
多次通信帶來(lái)的性能開(kāi)銷(xiāo)
Redis 事務(wù)的執(zhí)行需要客戶(hù)端與服務(wù)器之間多次交互:- 客戶(hù)端發(fā)送
MULTI
開(kāi)啟事務(wù)。 - 客戶(hù)端逐條發(fā)送事務(wù)中的命令。
- 客戶(hù)端發(fā)送
EXEC
提交事務(wù)。
這種交互模式相比 Lua 腳本一次性發(fā)送腳本的方式,性能要低。
- 客戶(hù)端發(fā)送
5.3 Lua 腳本更加靈活
Lua 腳本可以實(shí)現(xiàn)復(fù)雜的邏輯,包括條件判斷、循環(huán)等,而 Redis 事務(wù)只是一系列命令的簡(jiǎn)單打包,無(wú)法動(dòng)態(tài)調(diào)整邏輯。
示例:Lua 腳本實(shí)現(xiàn)條件判斷
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock_key "value"
上述腳本實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的條件判斷:僅當(dāng)鍵的值等于指定值時(shí)才刪除鍵。這樣的邏輯在事務(wù)中無(wú)法實(shí)現(xiàn)。
Lua 腳本與 Redis 事務(wù)的對(duì)比表
特性 | Lua 腳本 | Redis 事務(wù) |
---|---|---|
原子性 | 天然支持 | 部分支持(命令級(jí)原子性) |
錯(cuò)誤處理 | 全部失敗或全部成功 | 單命令失敗不影響其他命令 |
條件判斷 | 支持 | 不支持 |
性能 | 高(一次性發(fā)送腳本) | 較低(多次通信) |
實(shí)現(xiàn)復(fù)雜度 | 靈活(可實(shí)現(xiàn)復(fù)雜邏輯) | 簡(jiǎn)單(適合基礎(chǔ)操作) |
應(yīng)用場(chǎng)景 | 高級(jí)邏輯(如分布式鎖) | 基礎(chǔ)事務(wù)操作 |
實(shí)際場(chǎng)景中的選擇建議
-
使用 Lua 腳本的場(chǎng)景
- 需要條件判斷的復(fù)雜操作,例如分布式鎖的釋放時(shí)校驗(yàn)持有者身份。
- 多步操作需要原子性保障,例如獲取鎖的同時(shí)設(shè)置過(guò)期時(shí)間。
- 性能敏感的高并發(fā)場(chǎng)景。
-
使用 Redis 事務(wù)的場(chǎng)景
- 操作相對(duì)簡(jiǎn)單,且對(duì)回滾和條件判斷沒(méi)有要求。
- 確保操作序列的基本一致性,而不是嚴(yán)格的一致性。
- 對(duì)性能要求不高,或者已經(jīng)使用 Redis Pipeline 優(yōu)化通信延遲。
總結(jié)
- 性能:Lua 腳本因?yàn)闇p少了客戶(hù)端與 Redis 的通信開(kāi)銷(xiāo),性能優(yōu)于 Redis 事務(wù)。
- 靈活性:Lua 腳本能實(shí)現(xiàn)復(fù)雜邏輯,而事務(wù)只適合簡(jiǎn)單操作。
- 原子性:Lua 腳本具有天然的原子性,而 Redis 事務(wù)的原子性較為有限。
因此,在需要原子性保障和復(fù)雜邏輯的場(chǎng)景中(如分布式鎖),Lua 腳本通常是更優(yōu)的選擇。如果場(chǎng)景較簡(jiǎn)單且對(duì)一致性要求不高,可以考慮 Redis 事務(wù)。
六、對(duì)比與總結(jié)
6.1 Redis 分布式鎖與其他鎖實(shí)現(xiàn)方式的對(duì)比
分布式鎖的實(shí)現(xiàn)方式有多種,常見(jiàn)的有基于 Redis、數(shù)據(jù)庫(kù)、以及 Zookeeper 的方案。它們各自有不同的優(yōu)缺點(diǎn),適用于不同的業(yè)務(wù)需求。
6.1.1 基于 Redis 的分布式鎖
優(yōu)勢(shì)
- 性能優(yōu)越:基于內(nèi)存操作,讀寫(xiě)速度快,能夠支持高并發(fā)場(chǎng)景。
- 實(shí)現(xiàn)簡(jiǎn)單:通過(guò)單命令
SET
或 Lua 腳本可以快速實(shí)現(xiàn)分布式鎖。 - 靈活性強(qiáng):支持多種實(shí)現(xiàn)方式(單命令、Lua 腳本、RedLock 算法)。
劣勢(shì)
- 可靠性較低:單點(diǎn)故障可能導(dǎo)致鎖失效,需通過(guò)集群或 RedLock 增加容錯(cuò)性。
- 一致性問(wèn)題:鎖的釋放、超時(shí)機(jī)制對(duì)網(wǎng)絡(luò)延遲和時(shí)鐘同步敏感。
6.1.2 基于數(shù)據(jù)庫(kù)的分布式鎖
優(yōu)勢(shì)
- 一致性強(qiáng):數(shù)據(jù)庫(kù)天生支持事務(wù),能確保鎖的嚴(yán)格一致性。
- 依賴(lài)性低:無(wú)需額外引入中間件,只需數(shù)據(jù)庫(kù)即可實(shí)現(xiàn)。
劣勢(shì)
- 性能瓶頸:數(shù)據(jù)庫(kù)操作的性能低于內(nèi)存數(shù)據(jù)庫(kù),難以支撐高并發(fā)。
- 實(shí)現(xiàn)復(fù)雜:實(shí)現(xiàn)事務(wù)性鎖機(jī)制需要精心設(shè)計(jì)和調(diào)優(yōu)。
示例:基于 MySQL 的分布式鎖
通過(guò)使用 SELECT FOR UPDATE
來(lái)加鎖,但性能遠(yuǎn)不如 Redis。
6.1.3 基于 Zookeeper 的分布式鎖
優(yōu)勢(shì)
- 高可靠性:基于強(qiáng)一致性協(xié)議(如 ZAB 協(xié)議),在節(jié)點(diǎn)故障時(shí)仍能保證鎖的一致性。
- 天然分布式:適用于多節(jié)點(diǎn)、多數(shù)據(jù)中心部署。
劣勢(shì)
- 實(shí)現(xiàn)復(fù)雜:需要額外部署和維護(hù) Zookeeper 集群。
- 性能一般:不適合極高并發(fā)場(chǎng)景。
6.1.4 對(duì)比總結(jié)
特性 | Redis 分布式鎖 | 數(shù)據(jù)庫(kù)分布式鎖 | Zookeeper 分布式鎖 |
---|---|---|---|
性能 | ★★★★★ | ★★ | ★★★ |
一致性 | ★★★ | ★★★★★ | ★★★★★ |
實(shí)現(xiàn)復(fù)雜度 | ★★ | ★★★★ | ★★★ |
容錯(cuò)性 | ★★★ | ★★★ | ★★★★★ |
適用場(chǎng)景 | 高并發(fā)、低延遲 | 高一致性要求 | 跨節(jié)點(diǎn)高可靠性場(chǎng)景 |
6.2 實(shí)踐中的最佳建議
根據(jù) Redis 分布式鎖的特性,以下是實(shí)踐中需注意的關(guān)鍵點(diǎn):
-
使用
SET
替代SETNX
推薦使用SET key value NX EX
的方式獲取鎖,避免SETNX
與EXPIRE
組合操作導(dǎo)致的非原子性問(wèn)題。 -
使用唯一標(biāo)識(shí)避免誤刪
為鎖的值添加唯一標(biāo)識(shí)(如 UUID),并通過(guò) Lua 腳本釋放鎖,確保只有持鎖者能刪除鎖。 -
設(shè)計(jì)合理的超時(shí)時(shí)間
鎖的超時(shí)時(shí)間應(yīng)根據(jù)任務(wù)的預(yù)計(jì)執(zhí)行時(shí)間設(shè)置,避免鎖在任務(wù)未完成時(shí)過(guò)早失效。 -
在高可靠性場(chǎng)景中使用 RedLock
對(duì)于分布式系統(tǒng)的核心模塊(如訂單處理、支付系統(tǒng)),可考慮使用 RedLock 算法實(shí)現(xiàn)更高的容錯(cuò)性。 -
配合業(yè)務(wù)邏輯處理鎖失敗情況
在鎖獲取失敗時(shí),需明確業(yè)務(wù)邏輯的補(bǔ)償機(jī)制,如重試或降級(jí)處理。
6.3 Redis 分布式鎖的應(yīng)用建議
Redis 分布式鎖適合以下場(chǎng)景:
- 高并發(fā)環(huán)境:如限流、庫(kù)存控制。
- 中等可靠性要求:如日志處理、異步任務(wù)調(diào)度。
不適合的場(chǎng)景:
- 嚴(yán)格一致性需求:如金融交易,建議使用數(shù)據(jù)庫(kù)或 Zookeeper。
- 極高可靠性需求:如跨區(qū)域分布式事務(wù),建議結(jié)合其他技術(shù)(如 Kafka)。
6.4 總結(jié)
Redis 實(shí)現(xiàn)分布式鎖在現(xiàn)代分布式系統(tǒng)中占據(jù)重要地位,憑借其高性能和靈活性,廣泛應(yīng)用于高并發(fā)場(chǎng)景。通過(guò)結(jié)合正確的實(shí)現(xiàn)方式和實(shí)踐建議,可以進(jìn)一步提升鎖的可靠性。
然而,Redis 分布式鎖仍存在單點(diǎn)故障、網(wǎng)絡(luò)延遲等潛在問(wèn)題。未來(lái),可以通過(guò)以下方向改進(jìn):
- 更高效的 RedLock 算法:優(yōu)化鎖的容錯(cuò)性能,減少延遲對(duì)鎖的影響。
- 結(jié)合多種技術(shù)實(shí)現(xiàn)混合鎖:利用 Redis 提供性能,結(jié)合 Zookeeper 或數(shù)據(jù)庫(kù)保障一致性。
Redis 分布式鎖是開(kāi)發(fā)分布式系統(tǒng)的重要工具,但不是萬(wàn)能的。在實(shí)踐中,需根據(jù)業(yè)務(wù)需求選擇合適的鎖實(shí)現(xiàn)方式,打造高效、可靠的系統(tǒng)。
附錄
Redis 命令參考
- SET 命令官方文檔
- Lua 腳本官方文檔