說說 Redis 事務

更多技術文章,請關注我的個人部落格 www.immaxfang.com 和小公眾號 Max的學習札記

Redis 事務簡介

Redis 只是提供了簡單的事務功能。其本質是一組命令的集合,事務支援一次執行多個命令,在事務執行過程中,會順序執行隊列中的命令,其他客戶端提交的命令請求不會插入到本事務執行命令序列中。命令的執行過程是順序執行的,但不能保證原子性。無法像 MySQL 那樣,有隔離級別,出了問題之後還能回滾數據等高級操作。後面會詳細分析。

Redis 事務基本指令

Redis 提供了如下幾個事務相關的基礎指令。

  • MULTI開啟事務,Redis 會將後續命令加到隊列中,而不真正執行它們,直到後續使用EXEC來原子化的順序執行這些命令
  • EXEC執行所有事務塊內的命令
  • DISCARD取消事務,放棄執行事務塊內所有的命令
  • WATCH監視一個或多個 key,若事務在執行前,這些 key 被其他命令修改,則事務被終端,不會執行事務中的任何命令
  • UNWATCH取消 WATCH命令對所有 keys 的監視

一般情況下,一個簡單的 Redis 事務主要分為如下幾個部分:

  1. 執行命令MULTI開啟一個事務。
  2. 開啟事務之後,執行命令的多個命令會依次被放入一個隊列,放入成功則會返回QUEUED消息。
  3. 執行命令EXEC提交事務,Redis 會依次執行隊列中的命令,並依次返回所有命令的結果。(若想放棄提交事務,則執行DISCARD)。

下圖簡單介紹了下 Redis 事務執行的過程:
image.png

實例分析

下面我們來通過一些實際具體例子,來體會下 Redis 中的事務。前面我們也說到 Redis 的事務不是正真的事務,是無法完全滿足標準事務的ACID特性的。通過下面的例子,我們來看看,Redis 的「破產版」事務到底存在什麼問題。

  • [A]正常執行提交
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"

開啟事務後,提交的命令都會加入隊列(QUEUED),執行 EXEC 後會逐步執行命令並返回結果。這個看起來是不是和我們平時使用 MySQL 的事務操作相似,類似 start transaction 和 commit。

  • [B]正常取消事務
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)

開啟事務後,若不想繼續事務,使用 DISCARD 取消,前面提交的命令並不會真正執行,相關的 key 值不變。這個看起來也和 MySQL 的事務相似,類似 start transaction 和 rollback。

  • [C]WATCH 監視 key
-- 執行緒 1 中執行
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 中執行
----------------------------------------- 127.0.0.1:6379> SET a 2
----------------------------------------- OK
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> GET a
"2"

在開啟事務之前 WATCH 了 a 的值,隨後再開啟事務。在另一個執行緒中設置了 a 的值(SET a 2),然後再 EXEC 執行事務,結果為 nil,
說明事務沒有被執行。因為 a 的值在 WATCH 之後發生了變化,所以事務被取消了。

需要注意的是,這裡和開啟事務的時間點沒有關係,與 MULTI 和另一個執行緒設置 a 的值的先後沒有關係。只要是在 WATCH 之後發生了變化。無論事務是否已經開啟,執行事務(EXEC)的時候都會取消。
普通情況下,在執行 EXEC 和 DISCARD 命令時,都會默認執行 UNWATCH。

  • [D]語法錯誤
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"

當 Redis 開啟一個事務後,若添加的命令中有語法錯誤,會導致事務提交失敗。這種情況下事務隊列中的命令都不會被執行。如上面例子中 a 和 b 的值都是原有的值。
這類在 EXEC 之前產生的錯誤,如命令名稱錯誤,命令參數錯誤等,會在 EXEC 執行之前被檢測出來,所以在發生這些錯誤的時候,事務會被取消,事務中的所有命令都不會執行。(這種情況看起來是不是有點像回滾了)

  • [E]運行時錯誤
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"

當 Redis 開啟一個事務後,添加的命令沒有出現前面說的語法錯誤,但是在運行時檢測到了類型錯誤,導致事務最提交失敗(說未完全成功可能更準確點)。此時事務並不會回滾,而是跳過錯誤命令繼續執行。
如上面的例子,未報錯的命令值已經修改,a 被設置成了 1,b 被設置為了 hello,但是報錯的值未被修改,即 INCR b 類型錯誤,並未執行,b 的值也沒有被再更新。

Redis 事務與 ACID

通過上面的例子,我們已經知道 Redis 的事務和我們通常接觸的 MySQL 等關係資料庫的事務還有有一定差異的。它不保證原子性。同時 Redis 事務也沒有事務隔離級別的概念。下面我們來具體看下 Redis 在 ACID 四個特性中,那些是滿足的,那些是不滿足的。
事務執行可以分為命令入隊(EXEC 執行前)和命令實際執行(EXEC 執行之後)兩個階段。下面我們在分析的時候,很多時候都會分這兩種情況來分析。

  • 原子性(A)

上面的實例分析中,[A],[B],[C]三種正常的情況,我們可以很明顯的看出,是保證了原子性的
但是一些異常情況下,是不滿足原子性的。

  1. 如 [D] 所示的情況,客戶端發送的命令有語法錯誤,在命令入隊列時 Redis 就判斷出來了。等到執行 EXEC 命令時,Redis 就會拒絕執行所有提交的命令,返回事務失敗的結果。此種情況下,事務中的所有命令都不會被執行了,是保證了原子性的
  2. 如 [E] 所示的情況,事務操作入隊時,命令和操作類型不匹配,此時 Redis 沒有檢查出錯誤(這類錯誤是運行時錯誤)。等到執行 EXEC 命令後,Redis 實際執行這些命令操作時,就會報錯。需要注意的是,雖然 Redis 會對錯誤的命令報錯不執行,但是其餘正確的命令會依次執行完。此種情況下,是無法保證原子性的
  3. 在執行事務的 EXEC 命令時,Redis 實例發生了故障,導致事務執行失敗。此時,如果開啟了 AOF 日誌,那麼只會有部分事務操作被記錄到 AOF 日誌中。使用redis-check-aof工具檢測 AOF 日誌文件,可以把未完成的事務操作從 AOF 文件中去除。這樣一來,使用 AOF 文件恢復實例後,事務操作不會被再執行,從而保證了原子性。若使用的 RDB 模式,最新的 RDB 快照是在 EXEC 執行之前生成的,使用快照恢復之後,事務中的命令也都沒有執行,從而保證了原子性。若 Redis 沒有開啟持久化,則重啟後記憶體中的數據全部丟失,也就談不上原子性了。
  • 一致性(C)

一致性指的是事務執行前後,數據符合資料庫的定義和要求。這點在 Redis 事務中是滿足的,不論是發生語法錯誤還是運行時錯誤,錯誤的命令均不會被執行。

  1. EXEC 執行之前,入隊報錯(實例分析中的語法錯誤)

事務會放棄執行,故可以保證一致性。

  1. EXEC 執行之後,實際執行時報錯(實例分析中的運行時錯誤)

錯誤的命令不會被執行,正確的命令被執行,一致性可以保證。

  1. EXEC 執行時,實例宕機

若 Redis 沒有開啟持久化,實例宕機重啟後,數據都沒有了,數據是一致的。
若配置了 RDB 方式,RDB 快照不會在事務執行時執行。所以,若事務執行到一半,實例發生了故障,此時上一次 RDB 快照中不會包含事務所做的修改,而下一次 RDB 快照還沒有執行,實例重啟後,事務修改的數據會丟失,數據是一致的。若事務已經完成,但新一次的 RDB 快照還沒有生成,那事務修改的數據也會丟失,數據也是一致的。
若配置了 AOF 方式。當事務操作還沒被記錄到 AOF 日誌時,實例就發生故障了,使用 AOF 日誌恢復後數據是一致的。若事務中的只有部分操作被記錄到 AOF 日誌,可以使用 redis-check-aof清除事務中已經完成的操作,資料庫恢復後數據也是一致的。

  • 隔離性(I)
    1. 並發操作在 EXEC 執行前,隔離性需要通過 WATCH 機制來保證
    2. 並發操作在 EXEC 命令之後,隔離性可以保證

情況 a 可以參考前面的實例分析 WATCH 命令的使用。
情況 b,由於 Redis 是單執行緒執行命令,EXEC 命令執行後,Redis 會保證先把事務隊列中的所有命令執行完之後再執行之後的命令。

  • 持久性(D)

若 Redis 沒有開啟持久化,那麼就是所有數據都存儲在記憶體中,一旦重啟,數據就會丟失,因此此時事務的持久性是肯定無法得到保證的。
若 Redis 開啟了持久化,當實例宕機重啟,還是會有可能丟失數據,因此也並能完全保證持久性。
因此,我們可以說 Redis 事務無法一定保證持久性,僅在特殊的情況下,可以保證持久性。

關於 Redis 在開啟持久化之後,為啥還會丟失數據,筆者會單獨整理一篇 Redis 持久化與主從相關的文章來介紹,此處簡單說下。
如果配置了 RDB 模式,在一個事務執行後,下一次 RDB 快照還未執行前,Redis 實例發生了宕機,數據就會丟失、
如果配置了 AOF 模式,而 AOF 模式的三種配置選項 no,everysec,always 也都可能會產生數據丟失的情況。

總結一下,Redis 事務對 ACID 的支援情況:

  • 具備一定的原子性,但不支援回滾
  • 滿足一致性
  • 滿足隔離性
  • 無法保證持久性

Redis 事務為什麼不支援回滾

看一下官網的的說明:

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.

image.png
大部分需要事務回滾的情況是程式錯誤導致的,這種情況一般是開發環境,生產環境不應該出現這種錯誤。
對於邏輯錯誤,例如應該加 1,結果寫成了加 2,這種情況無法通過回滾來解決。
Redis 追求的是簡單高效,而傳統事務的實現相對複雜很多,這和 Redis 的設計思想是違背的。當我們享受 Redis 的快速時,也就無法再要求它更多。

總結

本文主要介紹了 Redis 事務的基礎指令與執行流程,並分析了其對傳統 ACID 特性支援的情況,相信大家對 Redis 事務已經有了一個簡單的了解。
通過上面的介紹,會發現 Redis 的事務似乎有點雞肋,確實實際中也很少會使用。至於事務的具體實現,筆者後續文章會結合源碼進行分析。今天的文章就到這裡,下期我們接著學。