Redis 事務支持 ACID 麼?

騰訊面試官:「數據庫事務機制瞭解麼?」

「內心獨白:小意思,不就 ACID 嘛,轉眼一想,我面試的可是技術專家,不會這麼簡單的問題吧」

程許遠:「balabala…… 極其自信且從容淡定的說了一通。」

騰訊面試官:「Redis 的事務瞭解麼?它的事務機制能實現 ACID 屬性麼?」

程許遠:「撓頭,這個……我知道 lua 腳本能實現事務…」

騰訊面試官:「好的,回去等通知吧。」


碼哥,我跟着你學習了 《Redis 系列》斬獲了很多 offer,沒想到最後敗在了 「Redis 如何實現事務?」這個問題上。

我們來一步步分析:

  1. 什麼是事務 ACID?
  2. Redis 如何實現事務?
  3. Redis 的事務能實現哪些屬性?
  4. Lua 腳本實現。

什麼是事務的 ACID

鬼吹燈之《雲南蟲谷》中的摸金校尉有句話叫「合則生,分則死」,爲了尋找雮塵珠他們三人分工明確、齊心協力共進退方可成功。

事務(Transaction)是併發控制單位,一個操作序列組合而成,這些操作要麼都執行,要麼都不執行。

「是一個不可分割的工作單位」。

事務在執行時,會提供專門的屬性保證:

  • 原子性(Atomicity):一個事務的多個操作必須完成,或者都不完成(ps:MySQL 的原子性靠什麼實現呢?歡迎留言區評論);

  • 一致性(Consistency):事務執行結束後,數據庫的完整性約束沒有被破壞,事務執行的前後順序都是合法數據狀態。

    數據庫的完整性約束包括但不限於:

    • 實體完整性(如行的主鍵存在且唯一);
    • 列完整性(如字段的類型、大小、長度要符合要求)
    • 外鍵約束;
    • 用戶自定義完整性(如轉賬前後,兩個賬戶餘額的和應該不變)。
  • 隔離性(Isolation):事務內部的操作與其他事務是隔離的,併發執行的各個事務之間不能互相干擾。

    講究的是不同事務之間的相互影響,嚴格的隔離性對應隔離級別中的可串行化(Serializable)。

  • 持久性(Durability):事務一旦提交,所有的修改將永久的保存到數據庫中,即使系統崩潰重啓後數據也不會丟失。

碼哥,瞭解了 ACID 的具體要求後,Redis 是如何實現事務機制呢?

Redis 如何實現事務

MULTIEXECDISCARD WATCH 命令是 Redis 實現事務的的基礎。

Redis 事務的執行過程包含三個步驟:

  1. 開啓事務;
  2. 命令入隊;
  3. 執行事務或丟棄;

顯式開啓一個事務

客戶端通過 MULTI 命令顯式地表示開啓一個事務,隨後的命令將排隊緩存,並不會實際執行。

命令入隊

客戶端把事務中的要執行的一系列指令發送到服務端。

需要注意的是,雖然指令發送到服務端,但是 Redis 實例只是把這一系列指令暫存在一個命令隊列中,並不會立刻執行。

執行事務或丟棄

客戶端向服務端發送提交或者丟棄事務的命令,讓 Redis 執行第二步中發送的具體指令或者清空隊列命令,放棄執行。

Redis 只需在調用 EXEC 時,即可安排隊列命令執行。

也可通過 DISCARD 丟棄第二步中保存在隊列中的命令。

Redis 事務案例

通過在線調試網站執行我們的樣例代碼:https://try.redis.io

正常執行

通過 MULTIEXEC 執行一個事務過程:

# 開啓事務
> MULTI
OK
# 開始定義一些列指令
> SET “公衆號:碼哥字節” "粉絲 100 萬"
QUEUED
> SET "order" "30"
QUEUED
> SET "文章數" 666
QUEUED
> GET "文章數"
QUEUED
# 實際執行事務
> EXEC
1) OK
2) OK
3) OK
4) "666"

我們看到每個讀寫指令執行後的返回結果都是 QUEUED,表示謝謝操作都被暫存到了命令隊列,還沒有實際執行。

當執行了 EXEC 命令,就可以看到具體每個指令的響應數據。

放棄事務

通過 MULTIDISCARD丟棄隊列命令:

# 初始化訂單數
> SET "order:mobile" 100
OK
# 開啓事務
> MULTI
OK
# 訂單 - 1
> DECR "order:mobile"
QUEUED
# 丟棄丟列命令
> DISCARD
OK
# 數據沒有被修改
> GET "order:mobile"
"100"

碼哥,Redis 的事務能保證 ACID 特性麼?

這個問題問得好,我們一起來分析下。

Redis 事務滿足 ACID?

Redis 事務可以一次執行多個命令, 並且帶有以下三個重要的保證:

  1. 批量指令在執行 EXEC 命令之前會放入隊列暫存;
  2. 收到 EXEC 命令後進入事務執行,事務中任意命令執行失敗,其餘的命令依然被執行;
  3. 事務執行過程中,其他客戶端提交的命令不會插入到當前命令執行的序列中。

原子性

碼哥,如果事務執行過程中發生錯誤了,原子性能保證麼?

在事務期間,可能遇到兩種命令錯誤:

  • 在執行 EXEC 命令前,發送的指令本身就錯誤。如下:
    • 參數數量錯誤;
    • 命令名稱錯誤,使用了不存在的命令;
    • 內存不足(Redis 實例使用 maxmemory指令配置內存限制)。
  • 在執行 EXEC 命令後,命令可能會失敗。例如,命令和操作的數據類型不匹配(對 String 類型 的 value 執行了 List 列表操作);
  • 在執行事務的 EXEC 命令時。 Redis 實例發生了故障導致事務執行失敗。

EXEC 執行前報錯

在命令入隊時,Redis 就會報錯並且記錄下這個錯誤

此時,我們還能繼續提交命令操作

等到執行了 EXEC命令之後,Redis 就會拒絕執行所有提交的命令操作,返回事務失敗的結果

這樣一來,事務中的所有命令都不會再被執行了,保證了原子性。

如下是指令入隊發生錯誤,導致事務失敗的例子:

#開啓事務
> MULTI
OK
#發送事務中的第一個操作,但是Redis不支持該命令,返回報錯信息
127.0.0.1:6379> PUT order 6
(error) ERR unknown command `PUT`, with args beginning with: `order`, `6`,
#發送事務中的第二個操作,這個操作是正確的命令,Redis把該命令入隊
> DECR b:stock
QUEUED
#實際執行事務,但是之前命令有錯誤,所以Redis拒絕執行
> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

EXEC 執行後報錯

事務操作入隊時,命令和操作的數據類型不匹配,但 Redis 實例沒有檢查出錯誤。

但是,在執行完 EXEC 命令以後,Redis 實際執行這些指令,就會報錯。

敲黑板了:Redis 雖然會對錯誤指令報錯,但是事務依然會把正確的命令執行完,這時候事務的原子性就無法保證了!

碼哥,爲什麼 Redis 不支持回滾?

其實,Redis 中並沒有提供回滾機制。雖然 Redis 提供了 DISCARD 命令。

但是,這個命令只能用來主動放棄事務執行,把暫存的命令隊列清空,起不到回滾的效果。

EXEC 執行時,發生故障

如果 Redis 開啓了 AOF 日誌,那麼,只會有部分的事務操作被記錄到 AOF 日誌中。

我們需要使用 redis-check-aof 工具檢查 AOF 日誌文件,這個工具可以把未完成的事務操作從 AOF 文件中去除。

這樣一來,我們使用 AOF 恢復實例後,事務操作不會再被執行,從而保證了原子性。

簡單總結

  • 命令入隊時就報錯,會放棄事務執行,保證原子性;
  • 命令入隊時沒報錯,實際執行時報錯,不保證原子性;
  • EXEC 命令執行時實例故障,如果開啓了 AOF 日誌,可以保證原子性。

一致性

一致性會受到錯誤命令、實例故障發生時機的影響,按照命令出錯實例故障兩個維度的發生時機,可以分三種情況分析。

EXEC 執行前,入隊報錯

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

EXEC 執行後,實際執行時報錯

有錯誤的執行不會執行,正確的指令可以正常執行,一致性可以保證。

EXEC 執行時,實例故障

實例故障後會進行重啓,這就和數據恢復的方式有關了,我們要根據實例是否開啓了 RDB 或 AOF 來分情況討論下。

如果我們沒有開啓 RDB 或 AOF,那麼,實例故障重啓後,數據都沒有了,數據庫是一致的。

如果我們使用了 RDB 快照,因爲 RDB 快照不會在事務執行時執行。

所以,事務命令操作的結果不會被保存到 RDB 快照中,使用 RDB 快照進行恢復時,數據庫裏的數據也是一致的。

如果我們使用了 AOF 日誌,而事務操作還沒有被記錄到 AOF 日誌時,實例就發生了故障,那麼,使用 AOF 日誌恢復的數據庫數據是一致的。

如果只有部分操作被記錄到了 AOF 日誌,我們可以使用 redis-check-aof 清除事務中已經完成的操作,數據庫恢復後也是一致的。

隔離性

事務執行又可以分成命令入隊(EXEC 命令執行前)和命令實際執行(EXEC 命令執行後)兩個階段。

所以在併發執行的時候我們針對這兩個階段分兩種情況分析:

  1. 併發操作在 EXEC 命令前執行,隔離性需要通過 WATCH 機制保證;
  2. 併發操作在 EXEC 命令之後,隔離性可以保證。

碼哥,什麼是 WATCH 機制?

我們重點來看第一種情況:一個事務的 EXEC 命令還沒有執行時,事務的命令操作是暫存在命令隊列中的。

此時,如果有其它的併發操作,同樣的 key 被修改,需要看事務是否使用了 WATCH 機制。

WATCH 機制的作用是:在事務執行前,監控一個或多個鍵的值變化情況,當事務調用 EXEC 命令執行時,WATCH 機制會先檢查監控的鍵是否被其它客戶端修改了。

如果修改了,就放棄事務執行,避免事務的隔離性被破壞。

同時,客戶端可以再次執行事務,此時,如果沒有併發修改事務數據的操作了,事務就能正常執行,隔離性也得到了保證。

沒有 WATCH

如果沒有 WATCH 機制, 在 EXEC 命令執行前的併發操作對數據讀寫。

當執行 EXEC 的時候,事務內部要操作的數據已經改變,Redis 並沒有做到事務之間的隔離。

併發操作在 EXEC 之後接收執行

至於第二種情況,因爲 Redis 是用單線程執行命令,而且,EXEC 命令執行後,Redis 會保證先把命令隊列中的所有命令執行完再執行之後的指令。

所以,在這種情況下,併發操作不會破壞事務的隔離性。

持久性

如果 Redis 沒有使用 RDB 或 AOF,那麼事務的持久化屬性肯定得不到保證。

如果 Redis 使用了 RDB 模式,那麼,在一個事務執行後,而下一次的 RDB 快照還未執行前,如果發生了實例宕機,數據丟失,這種情況下,事務修改的數據也是不能保證持久化的。

如果 Redis 採用了 AOF 模式,因爲 AOF 模式的三種配置選項 no、everysec 和 always 都會存在數據丟失的情況。

所以,事務的持久性屬性也還是得不到保證。

不管 Redis 採用什麼持久化模式,事務的持久性屬性是得不到保證的。

總結

  • Redis 具備了一定的原子性,但不支持回滾。
  • Redis 不具備 ACID 中一致性的概念。(或者說 Redis 在設計時就無視這點)
  • Redis 具備隔離性。
  • Redis 無法保證持久性。

Redis 的事務機制可以保證一致性和隔離性,但是無法保證持久性。

不過,因爲 Redis 本身是內存數據庫,持久性並不是一個必須的屬性,我們更加關注的還是原子性、一致性和隔離性這三個屬性。

原子性的情況比較複雜,當事務中使用的命令語法有誤時,原子性得不到保證,在其它情況下,事務都可以原子性執行。

好文推薦

Redis 核心篇:爲什麼這麼快

Redis 持久化篇:AOF 與 RDB 如何保證數據高可用

Redis 高可用篇:主從架構數據一致性同步原理

Redis 高可用篇:哨兵集羣原理

Redis 高可用篇:Cluster 集羣原理

Redis 實戰篇:巧用 Bitmap 實現億級海量數據統計

Redis 實戰篇:通過 Geo 類型實現附近的人邂逅女神

Redis 新特性篇:多線程模型解讀

Redis 6.0 新特性篇:客戶端緩存帶來的革命

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章