Redis 事務(wù)那些事兒:實(shí)用技巧和避坑指南!
一、為什么我們關(guān)心Redis事務(wù)?
在Java開發(fā)的日常工作中,Redis幾乎無處不在。你可能用它做緩存、排行榜、分布式鎖,甚至用它做輕量級(jí)的數(shù)據(jù)存儲(chǔ)。
但隨著業(yè)務(wù)復(fù)雜度提升,很多人都會(huì)遇到這樣的問題:
- 多個(gè)Redis操作需要保證原子性,怎么做?
- Redis的事務(wù)和MySQL事務(wù)一樣靠譜嗎?
- WATCH、MULTI、EXEC這些命令到底怎么用?能不能防止并發(fā)下的數(shù)據(jù)不一致?
這些問題看似簡(jiǎn)單,實(shí)則暗藏不少坑。今天這篇文章,我想用最實(shí)在的語(yǔ)言,把Redis事務(wù)的本質(zhì)、用法和注意事項(xiàng)講清楚,幫你在實(shí)際開發(fā)中少踩坑。
二、Redis事務(wù)機(jī)制全解析
1. Redis到底支不支持事務(wù)?
結(jié)論先行:Redis支持事務(wù),但和MySQL事務(wù)完全不是一回事。
MySQL事務(wù)強(qiáng)調(diào)ACID(原子性、一致性、隔離性、持久性),而Redis的事務(wù)機(jī)制更像是“命令打包、順序執(zhí)行”,沒有復(fù)雜的隔離和回滾機(jī)制。
2. Redis事務(wù)的基本命令和用法
Redis事務(wù)的核心命令有四個(gè):MULTI、EXEC、DISCARD、WATCH。
(1) MULTI/EXEC:事務(wù)的開始與提交
- MULTI:開啟事務(wù),后續(xù)命令進(jìn)入隊(duì)列
- EXEC:提交事務(wù),隊(duì)列中的命令依次執(zhí)行
舉個(gè)例子:
# 1. 初始化庫(kù)存
127.0.0.1:6379> set a:stock 100
OK
127.0.0.1:6379> set b:stock 200
OK
# 2. 開啟事務(wù)
127.0.0.1:6379> multi
OK
# 3. 將a:stock減1
127.0.0.1:6379> decr a:stock
QUEUED
# 4. 將b:stock減1
127.0.0.1:6379> decr b:stock
QUEUED
# 5. 實(shí)際執(zhí)行事務(wù)
127.0.0.1:6379> exec
1) (integer) 99
2) (integer) 199
127.0.0.1:6379>
(2) DISCARD:放棄事務(wù)
如果在MULTI之后,發(fā)現(xiàn)有問題,可以用DISCARD放棄事務(wù),清空命令隊(duì)列。
(3) WATCH:樂觀鎖的實(shí)現(xiàn)
在并發(fā)場(chǎng)景下,單靠MULTI/EXEC還不夠。比如轉(zhuǎn)賬操作,兩個(gè)客戶端同時(shí)讀取余額,都判斷可以轉(zhuǎn)賬,結(jié)果都扣了錢,余額就出錯(cuò)了。
這時(shí)候可以用WATCH命令,類似樂觀鎖。WATCH會(huì)監(jiān)控指定的key,如果在事務(wù)提交前這些key被其他客戶端修改,EXEC會(huì)失敗,事務(wù)不會(huì)執(zhí)行。
示例:
127.0.0.1:6379> get a:stock
"99"
127.0.0.1:6379> watch a:stock
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decr a:stock
QUEUED
127.0.0.1:6379> decr b:stock
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>
如上所示,a:stock在EXEC前被其他客戶端修改,EXEC會(huì)返回null,表示事務(wù)失敗。
三、Redis事務(wù)的常見“坑”和注意事項(xiàng)
1. 沒有回滾機(jī)制
只要EXEC執(zhí)行,前面的命令就算后面有錯(cuò),也不會(huì)回滾。比如:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name tom
QUEUED
127.0.0.1:6379> incr name
QUEUED
127.0.0.1:6379> set age 18
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379>
- set name tom執(zhí)行成功。
- incr name 執(zhí)行時(shí)報(bào)錯(cuò)(因?yàn)?name 是字符串,不能自增)。
- set age 18依然會(huì)被執(zhí)行。
注意:Redis事務(wù)中,某條命令出錯(cuò)不會(huì)影響其他命令的執(zhí)行,也不會(huì)回滾。
那如果我們想實(shí)現(xiàn)回滾的效果怎么辦呢?
2. 如何用DISCARD修復(fù)?
如果你在MULTI之后發(fā)現(xiàn)命令寫錯(cuò)了,可以在EXEC之前執(zhí)行DISCARD,這樣所有已入隊(duì)的命令都不會(huì)被執(zhí)行,數(shù)據(jù)不會(huì)被修改。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set addr bj
QUEUED
127.0.0.1:6379> incr addr
QUEUED
127.0.0.1:6379> set code 110
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get addr
(nil)
127.0.0.1:6379>
執(zhí)行結(jié)果:
- set addr bj、set code 110都不會(huì)被執(zhí)行。
- 事務(wù)被徹底放棄,Redis狀態(tài)不會(huì)有任何變化。
注意點(diǎn):
- 一旦執(zhí)行了EXEC,就無法再用DISCARD撤銷事務(wù),已經(jīng)執(zhí)行的命令不會(huì)回滾。
- DISCARD只能在事務(wù)提交前使用,相當(dāng)于“撤銷”本次事務(wù)。
3. 沒有隔離性
Redis事務(wù)期間,其他客戶端依然可以操作相關(guān)key。WATCH只能監(jiān)控key本身的變化,不能保證更復(fù)雜的業(yè)務(wù)一致性。
比如你WATCH了a:stock,但b:stock被其他客戶端修改了,你的事務(wù)依然會(huì)執(zhí)行。
四、實(shí)用干貨:Redis事務(wù)的正確打開方式
(1) 能用原子命令就用原子命令
Redis本身很多命令就是原子的,比如INCR、DECR、SETNX等,優(yōu)先用這些。
(2) 事務(wù)只保證命令的“批量、順序、一次性”執(zhí)行
不保證命令之間的隔離和回滾。
(3) WATCH適合樂觀鎖場(chǎng)景
比如扣庫(kù)存、轉(zhuǎn)賬等,先WATCH關(guān)鍵key,判斷條件后再M(fèi)ULTI/EXEC。
(4) 復(fù)雜業(yè)務(wù)建議用Lua腳本
Lua腳本在Redis中是原子執(zhí)行的,可以實(shí)現(xiàn)更復(fù)雜的業(yè)務(wù)邏輯和回滾。
(5) 不要把Redis事務(wù)當(dāng)成數(shù)據(jù)庫(kù)事務(wù)用
Redis事務(wù)和MySQL事務(wù)完全不是一回事,不能指望它幫你兜底所有一致性問題。
五、Redis事務(wù)和ACID的對(duì)比
很多同學(xué)會(huì)問:Redis事務(wù)到底支持ACID的哪幾項(xiàng)?
(1) 原子性(Atomicity)
Redis事務(wù)只保證“命令隊(duì)列”整體的原子性,不保證單條命令的原子性。EXEC時(shí),要么所有命令都執(zhí)行,要么都不執(zhí)行(WATCH監(jiān)控失敗時(shí))。
(2) 一致性(Consistency)
Redis事務(wù)本身不保證數(shù)據(jù)的一致性,需要開發(fā)者自己保證。
(3) 隔離性(Isolation)
Redis事務(wù)沒有嚴(yán)格的隔離性,事務(wù)執(zhí)行期間,其他客戶端可以修改相關(guān)key。
(4) 持久性(Durability)
取決于Redis的持久化配置(RDB、AOF),和事務(wù)機(jī)制本身無關(guān)。
一句話總結(jié):Redis事務(wù)只保證“命令批量執(zhí)行的原子性”,不保證隔離和回滾。
六、面試高頻問答
(1) Redis事務(wù)和MySQL事務(wù)的區(qū)別?
- MySQL事務(wù)支持ACID,Redis事務(wù)只保證命令批量執(zhí)行的原子性。
- MySQL事務(wù)有回滾機(jī)制,Redis事務(wù)沒有。
- MySQL事務(wù)有隔離級(jí)別,Redis事務(wù)沒有。
(2) Redis事務(wù)失敗會(huì)回滾嗎?
不會(huì)。只要EXEC執(zhí)行,前面的命令就算后面有錯(cuò),也不會(huì)回滾。
(3) WATCH命令的作用是什么?
實(shí)現(xiàn)樂觀鎖,監(jiān)控指定key,防止并發(fā)下的數(shù)據(jù)不一致。
(4) Redis事務(wù)適合哪些場(chǎng)景?
適合批量命令、簡(jiǎn)單樂觀鎖場(chǎng)景。不適合強(qiáng)一致性、復(fù)雜回滾的業(yè)務(wù)。