Redis系列12:Redis 的事務機制

2023-04-05 18:00:42

Redis系列1:深刻理解高效能Redis的本質
Redis系列2:資料持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 叢集模式
追求效能極致:Redis6.0的多執行緒模型
追求效能極致:使用者端快取帶來的革命
Redis系列8:Bitmap實現億萬級資料計算
Redis系列9:Geo 型別賦能億級地圖位置計算
Redis系列10:HyperLogLog實現海量資料基數統計
Redis系列11:記憶體淘汰策略

1 複習下何為事務機制?

Transaction(事務)是計算機的特有術語,它一般指單個邏輯工作單位,由一系列的操作組合而成,在這些操作執行的時候,要麼都執行成功,要麼都不執行,防止資料結果的不一致性。
簡而言之,事務是一個不可分割的工作邏輯單位。為了衡量工作單元是否具備事務能力,需要滿足四個特徵:ACID,即 原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、永續性(Durability)。

  • 原子性(Atomicity):一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
  • 一致性(Consistency):在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續資料庫可以自發性地完成預定的工作。
    • 實體完整性,存在唯一的主鍵
    • 列完整性:欄位型別、欄位長度等符合所有的預設規則
    • foreign key 外來鍵約束
    • 使用者自定義完整性(如使用者購物支付前後,商家收入和使用者的餘額總和不變)
  • 隔離性(Isolation):資料庫允許多個並行事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務並行執行時由於交叉執行而導致資料的不一致。事務隔離分為不同級別,包括讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和序列化(Serializable)。
  • 永續性(Durability):事務處理結束後,對資料的修改就是永久的,會持久化到硬碟上,即便系統故障也不會丟失。

2 Redis模式下如何實現事務機制?

Redis 支援事務機制,他實現事務的關鍵命令包括:

MULTI、EXEC、DISCARD 、 WATCH
  • MULTI 開啟事務,總是返回OK
  • EXEC 提交事務
  • DISCARD 放棄事務(即放棄提交執行)
  • WATCH 監控
  • QUEUED 命令加入執行的佇列,沒操作一個動作的時候,都先加入Queue

根據上述命令,Redis 事務的執行過程包含三個步驟:

  • 開啟事務:MULTI
  • 命令入隊:QUEUE
  • 執行事務或丟棄:EXEC 或者 DISCARD

2.1 顯式開啟一個事務

Client 通過 MULTI 命令顯式開啟一個事務,隨後執行的操作將會暫時快取在Queue中,實際並沒有立即執行。

2.2 將命令入佇列Queue

Client 端 把事務中的要執行的一系列操作指令傳送到Service 端。 Redis伺服器端 範例接收到指令之後,並不是馬上執行,而是暫存在命令佇列中。

2.3 執行事務或丟棄

當Client端向Service端傳送的命令都Ready了之後,可以傳送提交執行或者丟棄事務的命令,如果是執行則操作佇列中的具體指令,如果是丟棄則是清空佇列命令。

  • EXEC:執行佇列中的指令
  • DISCARD:丟棄儲存在佇列中的命令

2.4 EXEC命令執行範例

通過 MULTI 和 EXEC 執行一個事務過程:

#開啟事務
> MULTI
OK
# 定義一系列指令
> set 'name' 'brand'
QUEUED
> set 'age' 18
QUEUED
> INCR 'age'
QUEUED
> GET 'name'
QUEUED
> GET 'age'
QUEUED
# 實際執行事務
> EXEC
# 獲取執行結果
1) OK
2) OK
3) 19
4) "brand"
5) "19"

從上面可以看出來,每個讀寫指令執行後的返回結果都是 QUEUED,代表這些操作只是暫存在指令佇列中,並沒有實際執行。
當傳送了 EXEC 命令之後,才真正執行並獲取結果。

2.5 DISCARD命令:放棄事務

通過 MULTI 和 DISCARD 丟棄執行,清空指令佇列:

# 初始化訂資料
> SET 'name' 'brand'
OK
> SET 'age' 18
OK
# 開啟事務
> MULTI
OK
# 資料增量1
> INCR 'age'
QUEUED
# 丟棄
> DISCARD
OK
# 執行結果是增量前的資料
> get 'age'
"18"

2.6 因為命令錯誤導致的事務回滾

體現原子性,再發生故障的時候,要麼執行都成功,要麼執行都失敗

# 開啟事務
> MULTI
OK
# 初始一個資料
> SET 'age' 18
OK
# 對該資料進行更新,但Redis不支援該命令,返回報錯資訊
> UPD 'age' 17
(error) ERR unknown command `UPD`, with args beginning with: `age`, `17`,
# 繼續傳送一個指令 ,降低age的值,該指令是正確的
> DECR 'age'
QUEUED
# 執行exec,但是之前有錯誤,所以Redis放棄了事務,不再執行
> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

3 Redis事務機制能實現哪些屬性?

類似MySQL的事務,Redis 事務一次性可以執行多個指令, 而這多個指令通過以下的方式來保證:

  • EXEC 命令執行之前,所有的指令都是被暫存(Queued)在佇列中;
  • Service端接收到EXEC命令後開始執行事務,事務中某些命令執行失敗,其餘命令依舊執行;
  • 在事務執行的時候具備隔離性,其他Client端執行的指令不會亂入到當前指令的執行順序中的。

3.1 原子性(Atomicity)

在事務執行的過程中,可能遇到這幾種命令執行錯誤:

  • 在執行 EXEC 命令前,指令本身錯誤:
    • 引數數量不一致構成的錯誤
    • 命令名稱構成的錯誤,使用了不存在或者錯誤的命令:比如上面的 'UPD'
    • 超過MaxMemory記憶體限制,導致記憶體不足
  • 在執行 EXEC 命令後,命令的不合理操作導致的失敗。比如資料型別不匹配(對 String 型別 的 value 執行了 INCR 或者 DECR 之類的操作)
  • 在執行事務的 EXEC 命令時,範例故障導致的失敗,這種情況比較少一點。

3.1.1 EXEC 執行前報錯

執行前錯誤是指命令入隊(Queue)時,Redis 就會發現並記錄報錯。
即使執行了 EXEC命令之後,Redis也會拒絕執行指令佇列中的所有指令,返回事務失敗的結果。
這樣一來,所有的指令都不會被執行,保持了原子性。下面是指令入佇列的報錯的範例,跟上面的舉例一致:

# 開啟事務
> MULTI
OK
# 初始一個資料
> SET 'age' 18
OK
# 對該資料進行更新,但Redis不支援該命令,返回報錯資訊
> UPD 'age' 17
(error) ERR unknown command `UPD`, with args beginning with: `age`, `17`,
# 繼續傳送一個指令 ,降低age的值,該指令是正確的
> DECR 'age'
QUEUED
# 執行exec,但是之前有錯誤,所以Redis放棄了事務,不再執行
> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

3.1.2 EXEC 執行後報錯

這個跟上面的情況正好相反,指令入Queue時,命令的型別雖然不匹配,但是並沒有在預編譯的時候檢查出。
只有在EXEC 命令之後,實際執行指令的時候才會報錯。其他正確的指令還是會執行成功,不保證原子性。 參考下面:

# 開啟事務
> MULTI
OK
> set age 18
QUEUED
> set name 'brand'
QUEUED
> INCR age
QUEUED
# 這邊對String型別進行DECR,沒有報錯,但是在執行指令的時候會報錯誤
> DECR name
QUEUED
# 執行,會發現其他三條執行執行成功,只有一條執行失敗,返回報錯資訊
> EXEC
1) OK
2) OK
3) 19
4) ERR value is not an integer or out of range
# 檢視結果
> get name
"brand"
> get age
"19"

3.1.3 在EXEC執行時發生範例故障

可以使用AOF紀錄檔,把未完成的事務操作從AOF紀錄檔中去除,之後使用AOF進行恢復時就不會被再次執行,以此保證整個操作的原子性。
這個需要Redis啟用AOF紀錄檔這個持久化能力。

3.1.4 對於上述幾種錯誤特徵的總結

  • 指令入佇列時有報錯(所有指令中只要有一條不是QUEUED),就會放棄事務執行,保證原子性。如 3.1.1
  • 指令入佇列時沒報錯(所有指令都是QUEUED),但在實際執行EXEC時報錯,則不保證原子性。如 3.1.2
  • EXEC執行時出現故障,如果開啟了 AOF 紀錄檔,可以保證原子性。如 3.1.3

3.2 一致性

跟原子性類似,一致性會受到錯誤指令、執行異常、Redis故障等情況的影響,主要有如下幾種情況:

  • 指令入佇列時有報錯,事務被放棄執行,所以可以保證一致性。
  • 指令入佇列時正產,實際執行EXEC時報錯,則是錯誤部分不會執行,正確指令依舊正常執行,也可以保證一致性。
  • Redis範例故障分成幾種:
    • 未開啟持久化情況,故障重啟後資料都清空,結果是一致的。
    • RDB快照:事務命令操作的結果不被儲存到 RDB 快照中,所以在恢復時,資料結果是一致的。
    • AOF 紀錄檔:發生故障時,使用 redis-check-aof 清除事務中對應操作,資料庫恢復後也保持一致。

3.3 隔離性

從隔離性這個角度,事務執行的時機可以分成兩種:

  • 一種是操作在EXEC執行之前(純入隊期間),這時候採用 WATCH 的機制來保障
  • 另一種是開始執行EXEC之後(實際開始執行命令了),這時候本身具備隔離性了。

3.3.1 WATCH監測物件是否有變化

如果前後有變化,說明被修改了,這時就放棄事務執行,避免事務的隔離性被破壞。

3.3.2 對操作進行順序,並行操作排在 EXEC 之後

Redis 操作命令是單執行緒執行的,所以在EXEC 命令執行後,不會亂入其他操作,Redis 會保證把指令佇列中的所有指令都操作完成之後。
在執行後續的命令,所以,這種模式並行操作不會破壞事務的隔離性。它具有天然的隔離能力。

3.4 永續性

因為Redis的持久化特性,所以有如下三種可能性:

  • 未開啟 RDB快照 或 AOF紀錄檔,事務肯定不具備持久化能力。
  • RDB快照模式:我們在Redis持久化那一篇中聊過,RDB具有快照間隙,事務執行在快照之間則不會被保障。
  • AOF紀錄檔:無論紀錄檔持久化選項是 no、everysec 和 always 都會存在資料丟失的情況,所以也是無法完全保障的。
    所以不管 Redis 採用什麼持久化模式,事務的永續性屬性是得不到完全保證的。

4 總結

  • Redis 具備了一定的原子性,但不支援回滾。DISCARD 主要負責清空指令列表,放棄操作。
  • Redis 具備一致性的能力
  • Redis 具備隔離性的能力
  • Redis 無法保證永續性