簡單聊一聊Redis事務(wù)
沒錯,Redis也有事務(wù)管理,但是功能很簡單,在正式開發(fā)中也并不推薦使用。但是面試中有可能會問到,所以本文簡單談一談Redis的事務(wù)。
通過這篇文章,你會了解
- Redis為什么要提供事務(wù)?
- Redis事務(wù)基本指令和使用方法
- CAS樂觀鎖是什么?
- Redis事務(wù)為什么不支持回滾?
1. 為什么要用事務(wù)
我們知道Redis的單個命令是原子性的,比如get、set、mget、mset等指令。
原子性是指操作是不可分割的,在執(zhí)行完畢之前不會被任何其它任務(wù)或事件中斷,也就不會有并發(fā)的安全性問題
在涉及到多個命令的時(shí)候,如果需要把多個命令設(shè)置為一個不可分割的處理序列,就需要用到事務(wù)了。
比如,招財(cái)和陀螺各有100元,招財(cái)給陀螺轉(zhuǎn)了10元,這時(shí)候需要在Redis中把招財(cái)?shù)慕痤~總數(shù)-10,同時(shí)需要把陀螺的金額總數(shù)+10。這兩個操作要么同時(shí)成功,要么同時(shí)失敗,這時(shí)候就需要事務(wù)了。
實(shí)際上,Redis連這個簡單的需求都沒辦法完美做到,至于為啥,接著往下看吧
2. 事務(wù)的用法
2.1 5個基本指令
Redis提供了以下5個基本指令,先混個眼熟就行,接下來在案例中進(jìn)行實(shí)操,想記不住都難
- MULTI
- EXEC
- DISCARD
- WATCH
- UNWATCH
2.2 案例演示
案例場景:招財(cái)和陀螺各有100元,招財(cái)給陀螺轉(zhuǎn)了10元,這時(shí)候需要在Redis中把招財(cái)?shù)慕痤~-10,同時(shí)需要把陀螺的金額+10。
2.2.1 事務(wù)提交
我們首先為陀螺和招財(cái)初始化自己的金額;然后使用MULTI命令顯式開啟Redis事務(wù)。 該命令總是直接返回OK。此時(shí)用戶可以發(fā)送多個指令,Redis不會立刻執(zhí)行這些命令,而是將這些指令依次放入當(dāng)前事務(wù)的指令隊(duì)列中;EXEC被調(diào)用后,所有的命令才會被依次執(zhí)行。
# 給陀螺初始化100元
127.0.0.1:6379> set tuoluo 100
OK
# 給招財(cái)初始化100元
127.0.0.1:6379> set zhaocai 100
OK
# 顯式開啟事務(wù)
127.0.0.1:6379> MULTI
OK
# 給陀螺增加10元
127.0.0.1:6379(TX)> INCRBY tuoluo 10
QUEUED
# 給招財(cái)減少10元
127.0.0.1:6379(TX)> DECRBY zhaocai 10
QUEUED
# 執(zhí)行事務(wù)中的所有指令(提交事務(wù))
127.0.0.1:6379(TX)> EXEC
1) (integer) 110
2) (integer) 90
2.2.2 嵌套事務(wù)
Redis不支持嵌套事務(wù),多個MULTI命令和單個MULTI命令效果相同。
# 第一次開啟事務(wù)
127.0.0.1:6379> MULTI
OK
# 嘗試嵌套事務(wù)
127.0.0.1:6379(TX)> MULTI
(error) ERR MULTI calls can not be nested
# 仍然處于第一個事務(wù)當(dāng)中
127.0.0.1:6379(TX)>
2.2.3 放棄事務(wù)
如果開啟事務(wù)之后,中途后悔了怎么辦?調(diào)用DISCARD可以清空事務(wù)中的指令隊(duì)列,退出事務(wù)。
127.0.0.1:6379> MULTI
OK
# 在事務(wù)中調(diào)用DISCARD指令
127.0.0.1:6379(TX)> DISCARD
OK
# 會退出當(dāng)前事務(wù)
127.0.0.1:6379>
2.2.4 watch指令
假如我們在一個客戶端連接中開啟了事務(wù),另一個客戶端連接修改了這個事務(wù)涉及的變量值,將會怎樣?
client1開啟了一個轉(zhuǎn)賬的事務(wù),事務(wù)開始時(shí)招財(cái)和陀螺各自擁有100元,在執(zhí)行EXEC指令之前,client2將陀螺的余額添加了10元,此時(shí)執(zhí)行EXEC之后,陀螺最終的金額為120元,招財(cái)為90元。
很明顯,這種情況下存在數(shù)據(jù)安全問題。
為此Redis提供了WATCH的指令,該指令可以為Redis事務(wù)提供CAS樂觀鎖行為,即多個連接同時(shí)更新變量的時(shí)候,會和變量的初始值進(jìn)行比較,只在這個變量的值沒有被修改的情況下才會更新成新的值。
2.2.4.1 WATCH用法
對應(yīng)我們的案例,我們可以使用WATCH監(jiān)聽一個或多個key,如果開啟事務(wù)之前,至少有一個被監(jiān)視的key在EXEC執(zhí)行之前被修改了,那么整個事務(wù)都會被取消,直接返回nil(見下面的案例)。UNWATCH是WATCH的反操作。
2.2.4.2 CAS機(jī)制
CAS(Compare And Swap)比較并替換,是多并發(fā)時(shí)常用的一種樂觀鎖技術(shù)
CAS需要三個變量信息,分別是內(nèi)存位置(JAVA中的內(nèi)存地址,V),舊的預(yù)期值(A)和新值(B)。CAS執(zhí)行時(shí),當(dāng)且僅當(dāng)V和預(yù)期值A(chǔ)相等時(shí),更新V的值為新值B,否則不執(zhí)行更新。
3. 事務(wù)執(zhí)行出錯怎么辦
事務(wù)執(zhí)行時(shí)可能遇到問題,按照發(fā)生的時(shí)機(jī)不同分為兩種:
- 執(zhí)行EXEC之前
- 執(zhí)行EXEC之后
3.1 執(zhí)行EXEC之前發(fā)生錯誤
比如指令存在語法錯誤(參數(shù)數(shù)量不對,指令單詞拼錯)導(dǎo)致不能進(jìn)入commands隊(duì)列,這一步主要是編譯錯誤,還未到運(yùn)行時(shí)。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET tuoluo
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
這種情況下事務(wù)會執(zhí)行失敗,隊(duì)列中的所有指令都不會得到執(zhí)行。
3.2 執(zhí)行EXEC之后發(fā)生錯誤
這種錯誤往往是類型錯誤,比如對String使用了Hash的命令,這是運(yùn)行時(shí)錯誤,編譯期間不會出錯
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET tuoluo 100
QUEUED
127.0.0.1:6379(TX)> LPOP tuoluo
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
我們發(fā)現(xiàn),SET tuoluo 100的命令居然執(zhí)行成功了,也就是在發(fā)生了運(yùn)行異常的情況下,錯誤的指令不會被執(zhí)行,但是其他的命令不會受影響。
這種方式顯然不符合我們對原子性的定義,也就是Redis的事務(wù)無法實(shí)現(xiàn)原子性,無法保證數(shù)據(jù)一致。
針對這種缺陷,Redis官方也是做了說明的。
4. Redis事務(wù)為什么不支持回滾
引自Redis官方文檔。
為了方便大家理解,我翻譯一下就是:
- 你們程序員的鍋,關(guān)我們Redis屁事兒!
Redis官方認(rèn)為,只有在命令語法錯誤或者類型錯誤的時(shí)候,Redis命令才會執(zhí)行失敗。而且他們認(rèn)為有這種錯誤的語法一般也不會進(jìn)入到生產(chǎn)環(huán)境。而且不支持回滾可以使他們有更多時(shí)間玩兒Redis運(yùn)行得更簡單快捷。
這種說法多牛!如果出問題就是程序員的問題,寫錯了還讓代碼進(jìn)入生產(chǎn)環(huán)境,那就是罪上加罪,你永遠(yuǎn)賴不著Redis官方。
這可能就是不推薦使用Redis事務(wù)的原因了吧,雞肋是一方面,萬一被官方打臉了呢?所以Redis事務(wù)的知識稍微了解一下就好,面試被問到能回到上來就可以了。