一篇文章帶你徹底搞懂Redis?事務(wù)
Redis 事務(wù)簡(jiǎn)介
Redis 只是提供了簡(jiǎn)單的事務(wù)功能。其本質(zhì)是一組命令的集合,事務(wù)支持一次執(zhí)行多個(gè)命令,在事務(wù)執(zhí)行過(guò)程中,會(huì)順序執(zhí)行隊(duì)列中的命令,其他客戶端提交的命令請(qǐng)求不會(huì)插入到本事務(wù)執(zhí)行命令序列中。命令的執(zhí)行過(guò)程是順序執(zhí)行的,但不能保證原子性。無(wú)法像 MySQL 那樣,有隔離級(jí)別,出了問(wèn)題之后還能回滾數(shù)據(jù)等高級(jí)操作。后面會(huì)詳細(xì)分析。
Redis 事務(wù)基本指令
Redis 提供了如下幾個(gè)事務(wù)相關(guān)的基礎(chǔ)指令。
MULTI
開(kāi)啟事務(wù),Redis 會(huì)將后續(xù)命令加到隊(duì)列中,而不真正執(zhí)行它們,直到后續(xù)使用EXEC
來(lái)原子化的順序執(zhí)行這些命令 EXEC
執(zhí)行所有事務(wù)塊內(nèi)的命令 DISCARD
取消事務(wù),放棄執(zhí)行事務(wù)塊內(nèi)所有的命令 WATCH
監(jiān)視一個(gè)或多個(gè) key,若事務(wù)在執(zhí)行前,這些 key 被其他命令修改,則事務(wù)被終端,不會(huì)執(zhí)行事務(wù)中的任何命令 UNWATCH
取消 WATCH
命令對(duì)所有 keys 的監(jiān)視
一般情況下,一個(gè)簡(jiǎn)單的 Redis 事務(wù)主要分為如下幾個(gè)部分:
執(zhí)行命令MULTI
開(kāi)啟一個(gè)事務(wù)。 開(kāi)啟事務(wù)之后,執(zhí)行命令的多個(gè)命令會(huì)依次被放入一個(gè)隊(duì)列,放入成功則會(huì)返回QUEUED
消息。 執(zhí)行命令EXEC
提交事務(wù),Redis 會(huì)依次執(zhí)行隊(duì)列中的命令,并依次返回所有命令的結(jié)果。(若想放棄提交事務(wù),則執(zhí)行DISCARD
)。
下圖簡(jiǎn)單介紹了下 Redis 事務(wù)執(zhí)行的過(guò)程:
實(shí)例分析
下面我們來(lái)通過(guò)一些實(shí)際具體例子,來(lái)體會(huì)下 Redis 中的事務(wù)。前面我們也說(shuō)到 Redis 的事務(wù)不是正真的事務(wù),是無(wú)法完全滿足標(biāo)準(zhǔn)事務(wù)的ACID
特性的。通過(guò)下面的例子,我們來(lái)看看,Redis 的“破產(chǎn)版”事務(wù)到底存在什么問(wèn)題。
[A]正常執(zhí)行提交
127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET a 1 QUEUED 127.0.0.1:6379> SET b 2 QUEUED 127.0.0.1:6379> EXEC 1) OK 2) OK 127.0.0.1:6379> GET a "1" 127.0.0.1:6379> GET b "2"
開(kāi)啟事務(wù)后,提交的命令都會(huì)加入隊(duì)列(QUEUED),執(zhí)行 EXEC 后會(huì)逐步執(zhí)行命令并返回結(jié)果。這個(gè)看起來(lái)是不是和我們平時(shí)使用 MySQL 的事務(wù)操作相似,類似 start transaction 和 commit。
[B]正常取消事務(wù)
127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET a 1 QUEUED 127.0.0.1:6379> SET b 2 QUEUED 127.0.0.1:6379> DISCARD OK 127.0.0.1:6379> 127.0.0.1:6379> GET a (nil) 127.0.0.1:6379> GET b (nil)
開(kāi)啟事務(wù)后,若不想繼續(xù)事務(wù),使用 DISCARD 取消,前面提交的命令并不會(huì)真正執(zhí)行,相關(guān)的 key 值不變。這個(gè)看起來(lái)也和 MySQL 的事務(wù)相似,類似 start transaction 和 rollback。
[C]WATCH 監(jiān)視 key
-- 線程 1 中執(zhí)行 127.0.0.1:6379> del a (integer) 1 127.0.0.1:6379> get a (nil) 127.0.0.1:6379> SET a 0 OK 127.0.0.1:6379> WATCH a OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET a 1 QUEUED ----------------------------------------- 線程 2 中執(zhí)行 ----------------------------------------- 127.0.0.1:6379> SET a 2 ----------------------------------------- OK 127.0.0.1:6379> EXEC (nil) 127.0.0.1:6379> GET a "2"
在開(kāi)啟事務(wù)之前 WATCH 了 a 的值,隨后再開(kāi)啟事務(wù)。在另一個(gè)線程中設(shè)置了 a 的值(SET a 2),然后再 EXEC 執(zhí)行事務(wù),結(jié)果為 nil,
說(shuō)明事務(wù)沒(méi)有被執(zhí)行。因?yàn)?a 的值在 WATCH 之后發(fā)生了變化,所以事務(wù)被取消了。
需要注意的是,這里和開(kāi)啟事務(wù)的時(shí)間點(diǎn)沒(méi)有關(guān)系,與 MULTI 和另一個(gè)線程設(shè)置 a 的值的先后沒(méi)有關(guān)系。只要是在 WATCH 之后發(fā)生了變化。無(wú)論事務(wù)是否已經(jīng)開(kāi)啟,執(zhí)行事務(wù)(EXEC)的時(shí)候都會(huì)取消。
普通情況下,在執(zhí)行 EXEC 和 DISCARD 命令時(shí),都會(huì)默認(rèn)執(zhí)行 UNWATCH。
[D]語(yǔ)法錯(cuò)誤
127.0.0.1:6379> SET a 1 OK 127.0.0.1:6379> SET b 2 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET a 11 QUEUED 127.0.0.1:6379> SETS b 22 (error) ERR unknown command 'SETS' 127.0.0.1:6379> EXEC (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> GET a "1" 127.0.0.1:6379> GET b "2"
當(dāng) Redis 開(kāi)啟一個(gè)事務(wù)后,若添加的命令中有語(yǔ)法錯(cuò)誤,會(huì)導(dǎo)致事務(wù)提交失敗。這種情況下事務(wù)隊(duì)列中的命令都不會(huì)被執(zhí)行。如上面例子中 a 和 b 的值都是原有的值。
這類在 EXEC 之前產(chǎn)生的錯(cuò)誤,如命令名稱錯(cuò)誤,命令參數(shù)錯(cuò)誤等,會(huì)在 EXEC 執(zhí)行之前被檢測(cè)出來(lái),所以在發(fā)生這些錯(cuò)誤的時(shí)候,事務(wù)會(huì)被取消,事務(wù)中的所有命令都不會(huì)執(zhí)行。(這種情況看起來(lái)是不是有點(diǎn)像回滾了)
[E]運(yùn)行時(shí)錯(cuò)誤
127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET a 1 QUEUED 127.0.0.1:6379> SET b hello QUEUED 127.0.0.1:6379> INCR b QUEUED 127.0.0.1:6379> EXEC 1) OK 2) OK 3) (error) ERR value is not an integer or out of range 127.0.0.1:6379> GET a "1" 127.0.0.1:6379> GET b "hello"
當(dāng) Redis 開(kāi)啟一個(gè)事務(wù)后,添加的命令沒(méi)有出現(xiàn)前面說(shuō)的語(yǔ)法錯(cuò)誤,但是在運(yùn)行時(shí)檢測(cè)到了類型錯(cuò)誤,導(dǎo)致事務(wù)最提交失敗(說(shuō)未完全成功可能更準(zhǔn)確點(diǎn))。此時(shí)事務(wù)并不會(huì)回滾,而是跳過(guò)錯(cuò)誤命令繼續(xù)執(zhí)行。
如上面的例子,未報(bào)錯(cuò)的命令值已經(jīng)修改,a 被設(shè)置成了 1,b 被設(shè)置為了 hello,但是報(bào)錯(cuò)的值未被修改,即 INCR b 類型錯(cuò)誤,并未執(zhí)行,b 的值也沒(méi)有被再更新。
Redis 事務(wù)與 ACID
通過(guò)上面的例子,我們已經(jīng)知道 Redis 的事務(wù)和我們通常接觸的 MySQL 等關(guān)系數(shù)據(jù)庫(kù)的事務(wù)還有有一定差異的。它不保證原子性。同時(shí) Redis 事務(wù)也沒(méi)有事務(wù)隔離級(jí)別的概念。下面我們來(lái)具體看下 Redis 在 ACID 四個(gè)特性中,那些是滿足的,那些是不滿足的。
事務(wù)執(zhí)行可以分為命令入隊(duì)(EXEC 執(zhí)行前)和命令實(shí)際執(zhí)行(EXEC 執(zhí)行之后)兩個(gè)階段。下面我們?cè)诜治龅臅r(shí)候,很多時(shí)候都會(huì)分這兩種情況來(lái)分析。
原子性(A)
上面的實(shí)例分析中,[A],[B],[C]三種正常的情況,我們可以很明顯的看出,是保證了原子性的。
但是一些異常情況下,是不滿足原子性的。
如 [D] 所示的情況,客戶端發(fā)送的命令有語(yǔ)法錯(cuò)誤,在命令入隊(duì)列時(shí) Redis 就判斷出來(lái)了。等到執(zhí)行 EXEC 命令時(shí),Redis 就會(huì)拒絕執(zhí)行所有提交的命令,返回事務(wù)失敗的結(jié)果。此種情況下,事務(wù)中的所有命令都不會(huì)被執(zhí)行了,是保證了原子性的。 如 [E] 所示的情況,事務(wù)操作入隊(duì)時(shí),命令和操作類型不匹配,此時(shí) Redis 沒(méi)有檢查出錯(cuò)誤(這類錯(cuò)誤是運(yùn)行時(shí)錯(cuò)誤)。等到執(zhí)行 EXEC 命令后,Redis 實(shí)際執(zhí)行這些命令操作時(shí),就會(huì)報(bào)錯(cuò)。需要注意的是,雖然 Redis 會(huì)對(duì)錯(cuò)誤的命令報(bào)錯(cuò)不執(zhí)行,但是其余正確的命令會(huì)依次執(zhí)行完。此種情況下,是無(wú)法保證原子性的。 在執(zhí)行事務(wù)的 EXEC 命令時(shí),Redis 實(shí)例發(fā)生了故障,導(dǎo)致事務(wù)執(zhí)行失敗。此時(shí),如果開(kāi)啟了 AOF 日志,那么只會(huì)有部分事務(wù)操作被記錄到 AOF 日志中。使用redis-check-aof
工具檢測(cè) AOF 日志文件,可以把未完成的事務(wù)操作從 AOF 文件中去除。這樣一來(lái),使用 AOF 文件恢復(fù)實(shí)例后,事務(wù)操作不會(huì)被再執(zhí)行,從而保證了原子性。若使用的 RDB 模式,最新的 RDB 快照是在 EXEC 執(zhí)行之前生成的,使用快照恢復(fù)之后,事務(wù)中的命令也都沒(méi)有執(zhí)行,從而保證了原子性。若 Redis 沒(méi)有開(kāi)啟持久化,則重啟后內(nèi)存中的數(shù)據(jù)全部丟失,也就談不上原子性了。 一致性(C)
一致性指的是事務(wù)執(zhí)行前后,數(shù)據(jù)符合數(shù)據(jù)庫(kù)的定義和要求。這點(diǎn)在 Redis 事務(wù)中是滿足的,不論是發(fā)生語(yǔ)法錯(cuò)誤還是運(yùn)行時(shí)錯(cuò)誤,錯(cuò)誤的命令均不會(huì)被執(zhí)行。
EXEC 執(zhí)行之前,入隊(duì)報(bào)錯(cuò)(實(shí)例分析中的語(yǔ)法錯(cuò)誤)
事務(wù)會(huì)放棄執(zhí)行,故可以保證一致性。
EXEC 執(zhí)行之后,實(shí)際執(zhí)行時(shí)報(bào)錯(cuò)(實(shí)例分析中的運(yùn)行時(shí)錯(cuò)誤)
錯(cuò)誤的命令不會(huì)被執(zhí)行,正確的命令被執(zhí)行,一致性可以保證。
EXEC 執(zhí)行時(shí),實(shí)例宕機(jī)
若 Redis 沒(méi)有開(kāi)啟持久化,實(shí)例宕機(jī)重啟后,數(shù)據(jù)都沒(méi)有了,數(shù)據(jù)是一致的。
若配置了 RDB 方式,RDB 快照不會(huì)在事務(wù)執(zhí)行時(shí)執(zhí)行。所以,若事務(wù)執(zhí)行到一半,實(shí)例發(fā)生了故障,此時(shí)上一次 RDB 快照中不會(huì)包含事務(wù)所做的修改,而下一次 RDB 快照還沒(méi)有執(zhí)行,實(shí)例重啟后,事務(wù)修改的數(shù)據(jù)會(huì)丟失,數(shù)據(jù)是一致的。若事務(wù)已經(jīng)完成,但新一次的 RDB 快照還沒(méi)有生成,那事務(wù)修改的數(shù)據(jù)也會(huì)丟失,數(shù)據(jù)也是一致的。
若配置了 AOF 方式。當(dāng)事務(wù)操作還沒(méi)被記錄到 AOF 日志時(shí),實(shí)例就發(fā)生故障了,使用 AOF 日志恢復(fù)后數(shù)據(jù)是一致的。若事務(wù)中的只有部分操作被記錄到 AOF 日志,可以使用 redis-check-aof
清除事務(wù)中已經(jīng)完成的操作,數(shù)據(jù)庫(kù)恢復(fù)后數(shù)據(jù)也是一致的。
隔離性(I) 并發(fā)操作在 EXEC 執(zhí)行前,隔離性需要通過(guò) WATCH 機(jī)制來(lái)保證 并發(fā)操作在 EXEC 命令之后,隔離性可以保證
情況 a 可以參考前面的實(shí)例分析 WATCH 命令的使用。
情況 b,由于 Redis 是單線程執(zhí)行命令,EXEC 命令執(zhí)行后,Redis 會(huì)保證先把事務(wù)隊(duì)列中的所有命令執(zhí)行完之后再執(zhí)行之后的命令。
持久性(D)
若 Redis 沒(méi)有開(kāi)啟持久化,那么就是所有數(shù)據(jù)都存儲(chǔ)在內(nèi)存中,一旦重啟,數(shù)據(jù)就會(huì)丟失,因此此時(shí)事務(wù)的持久性是肯定無(wú)法得到保證的。
若 Redis 開(kāi)啟了持久化,當(dāng)實(shí)例宕機(jī)重啟,還是會(huì)有可能丟失數(shù)據(jù),因此也并能完全保證持久性。
因此,我們可以說(shuō) Redis 事務(wù)無(wú)法一定保證持久性,僅在特殊的情況下,可以保證持久性。
關(guān)于 Redis 在開(kāi)啟持久化之后,為啥還會(huì)丟失數(shù)據(jù),筆者會(huì)單獨(dú)整理一篇 Redis 持久化與主從相關(guān)的文章來(lái)介紹,此處簡(jiǎn)單說(shuō)下。
如果配置了 RDB 模式,在一個(gè)事務(wù)執(zhí)行后,下一次 RDB 快照還未執(zhí)行前,Redis 實(shí)例發(fā)生了宕機(jī),數(shù)據(jù)就會(huì)丟失、
如果配置了 AOF 模式,而 AOF 模式的三種配置選項(xiàng) no,everysec,always 也都可能會(huì)產(chǎn)生數(shù)據(jù)丟失的情況。
總結(jié)一下,Redis 事務(wù)對(duì) ACID 的支持情況:
具備一定的原子性,但不支持回滾 滿足一致性 滿足隔離性 無(wú)法保證持久性 Redis 事務(wù)為什么不支持回滾
看一下官網(wǎng)的的說(shuō)明:
What about rollbacks?
Redis does not support rollbacks of transactions since supporting rollbacks would have a significant impact on the simplicity and performance of Redis.
大部分需要事務(wù)回滾的情況是程序錯(cuò)誤導(dǎo)致的,這種情況一般是開(kāi)發(fā)環(huán)境,生產(chǎn)環(huán)境不應(yīng)該出現(xiàn)這種錯(cuò)誤。
對(duì)于邏輯錯(cuò)誤,例如應(yīng)該加 1,結(jié)果寫成了加 2,這種情況無(wú)法通過(guò)回滾來(lái)解決。
Redis 追求的是簡(jiǎn)單高效,而傳統(tǒng)事務(wù)的實(shí)現(xiàn)相對(duì)復(fù)雜很多,這和 Redis 的設(shè)計(jì)思想是違背的。當(dāng)我們享受 Redis 的快速時(shí),也就無(wú)法再要求它更多。
總結(jié)
本文主要介紹了 Redis 事務(wù)的基礎(chǔ)指令與執(zhí)行流程,并分析了其對(duì)傳統(tǒng) ACID 特性支持的情況,相信大家對(duì) Redis 事務(wù)已經(jīng)有了一個(gè)簡(jiǎn)單的了解。
通過(guò)上面的介紹,會(huì)發(fā)現(xiàn) Redis 的事務(wù)似乎有點(diǎn)雞肋,確實(shí)實(shí)際中也很少會(huì)使用。至于事務(wù)的具體實(shí)現(xiàn),筆者后續(xù)文章會(huì)結(jié)合源碼進(jìn)行分析。今天的文章就到這里,下期我們接著學(xué)。
到此這篇關(guān)于一篇文章帶你徹底搞懂Redis 事務(wù)的文章就介紹到這了,更多相關(guān)Redis 事務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis+threading實(shí)現(xiàn)多線程消息隊(duì)列的使用示例
Redis多線程消息隊(duì)列是一種使用Redis作為存儲(chǔ)后端的消息隊(duì)列實(shí)現(xiàn),它利用Redis的線程并發(fā)處理能力來(lái)提高消息隊(duì)列的處理效率,本文主要介紹了Redis+threading實(shí)現(xiàn)多線程消息隊(duì)列的使用示例,感興趣的可以了解一下2023-12-12緩存替換策略及應(yīng)用(以Redis、InnoDB為例)
本文以Redis、InnoDB為例給大家講解緩存替換策略及應(yīng)用,本文給大家提到五種置換策略,通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-07-07Redis查看KEY的數(shù)據(jù)類型的方法和步驟
在Redis中,可以使用 TYPE 命令來(lái)查看指定key的數(shù)據(jù)類型,該命令會(huì)返回存儲(chǔ)在指定key中的值的數(shù)據(jù)類型,本文給大家介紹了具體的使用方法和步驟,感興趣的朋友可以參考下2024-04-04Redis高并發(fā)情況下并發(fā)扣減庫(kù)存項(xiàng)目實(shí)戰(zhàn)
本文主要介紹了Redis高并發(fā)情況下并發(fā)扣減庫(kù)存項(xiàng)目實(shí)戰(zhàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04通過(guò)實(shí)例解析布隆過(guò)濾器工作原理及實(shí)例
這篇文章主要介紹了通過(guò)實(shí)例解析布隆過(guò)濾器工作原理及實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11python腳本實(shí)現(xiàn)Redis未授權(quán)批量提權(quán)
這篇文章主要給大家介紹了關(guān)于利用python腳本實(shí)現(xiàn)redis未授權(quán)批量提權(quán)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09Redis+Caffeine實(shí)現(xiàn)多級(jí)緩存的步驟
隨著不斷的發(fā)展,這一架構(gòu)也產(chǎn)生了改進(jìn),在一些場(chǎng)景下可能單純使用Redis類的遠(yuǎn)程緩存已經(jīng)不夠了,還需要進(jìn)一步配合本地緩存使用,例如Guava cache或Caffeine,從而再次提升程序的響應(yīng)速度與服務(wù)性能,這篇文章主要介紹了Redis+Caffeine實(shí)現(xiàn)多級(jí)緩存,需要的朋友可以參考下2024-01-01