Redis學習筆記系列(九)——Redis事務

7. Redis事務

7.1 事務定義

Redis 通過MULTIDISCARDEXECWATCH四個命令來實現事務功能。Redis中的事務同命令一樣都是Redis的最小執行單位,一個事務中的命令要么都執行,要么都不執行。事務的原理是先將屬于一個事務的命令發送給Redis,然后再讓Redis依次執行這些命令。下面,我們用一個示例來演示Redis事務。
假設A有10元錢,B有0元錢,A向B轉5元錢,那么這個過程就需要用事務來實現,以保證A減5元錢和B增加5元錢要么都成功,要么都失敗。

127.0.0.1:6379> SET A 10
OK
127.0.0.1:6379> SET B 0
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCRBY A -5
QUEUED
127.0.0.1:6379> INCRBY B 5
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 5
2) (integer) 5
127.0.0.1:6379> GET A
"5"
127.0.0.1:6379> GET B
"5"

Redis事務可以保證以下兩點:

  1. Redis保證一個事務中的所有命令要么都執行,要么都不執行。如果在發送EXEC命令前客戶端斷線了,則Redis會清空事務隊列,事務中的所有命令都不會執行。而一旦客戶端發送了EXEC命令,所有的命令就都會被執行,即使此后客戶端斷線也沒關系,因為Redis中已經記錄了所有要執行的命令。
  2. Redis的事務還能保證一個事務內的命令依次執行而不被其他命令插入。試想客戶端A需要執行幾條命令,同時客戶端B發送了一條命令,如果不使用事務,則客戶端B的命令可能會插入到客戶端A的幾條命令中執行。如果不希望發生這種情況,也可以使用事務。

7.2 事務的實現機制

一個事務從開始到執行會經歷以下三個階段:

  • 開始事務。
  • 命令入隊。
  • 執行事務。
7.2.1 開始事務

MULTI命令的執行標記著事務的開始,該命令的唯一作用就是將客戶端的REDIS_MULTI選項打開, 讓客戶端從非事務狀態切換到事務狀態。

127.0.0.1:6379> MULTI
OK
7.2.2 命令入隊

當客戶端處于非事務狀態下時, 所有發送給服務器端的命令都會立即被服務器執行。但是,當客戶端進入事務狀態之后,服務器在收到來自客戶端的命令時,不會立即執行命令,而是將這些命令全部放進一個事務隊列里,然后返回 QUEUED ,表示命令已入隊。

127.0.0.1:6379> INCRBY A -5
QUEUED
127.0.0.1:6379> INCRBY B 5
QUEUED
7.2.3 執行事務

前面說到,當客戶端進入事務狀態之后,客戶端發送的命令就會被放進事務隊列里。但其實并不是所有的命令都會被放進事務隊列, 其中的例外就是 EXECDISCARDMULTIWATCH 這四個命令 —— 當這四個命令從客戶端發送到服務器時, 它們會像客戶端處于非事務狀態一樣, 直接被服務器執行。因此,如果客戶端正處于事務狀態, 那么當 EXEC 命令執行時, 服務器根據客戶端所保存的事務隊列, 以先進先出(FIFO)的方式執行事務隊列中的命令: 最先入隊的命令最先執行, 而最后入隊的命令最后執行。

127.0.0.1:6379> EXEC
1) (integer) 5
2) (integer) 5

事務狀態下,除了EXEC 命令會立刻執行外,DISCARDMULTIWATCH 也會立刻執行。
DISCARD 命令用于取消一個事務, 它清空客戶端的整個事務隊列, 然后將客戶端從事務狀態調整回非事務狀態, 最后返回字符串 OK 給客戶端, 說明事務已被取消。
Redis 的事務是不可嵌套的, 當客戶端已經處于事務狀態, 而客戶端又再向服務器發送 MULTI 時, 服務器只是簡單地向客戶端發送一個錯誤, 然后繼續等待其他命令的入隊。 MULTI 命令的發送不會造成整個事務失敗, 也不會修改事務隊列中已有的數據。
WATCH只能在客戶端進入事務狀態之前執行, 在事務狀態下發送 WATCH命令會引發一個錯誤, 但它不會造成整個事務失敗, 也不會修改事務隊列中已有的數據。

7.3 WATCH的作用

WATCH 命令用于在事務開始之前監視任意數量的鍵: 當調用 EXEC 命令執行事務時, 如果任意一個被監視的鍵已經被其他客戶端修改了, 那么整個事務不再執行, 直接返回失敗。示例如下:

# 監控k1
127.0.0.1:6379> WATCH k1
OK
# 開始事務
127.0.0.1:6379> MULTI
OK
# 設置k1的值為v2
127.0.0.1:6379> SET k1 v2
QUEUED
# 設置失敗
127.0.0.1:6379> EXEC
(nil)
#  獲取k1為v1,顯然是別的客戶端修改了k1的值
127.0.0.1:6379> GET k1
"v1"

失敗原因:

時間 客戶端A 客戶端B
T1 WATCH k1
T2 MULTI
T3 SET k1 v2
T4 SET k1 v1
T5 EXEC

由于T4時刻,客戶端B執行了SET k1 v1,當客戶端 A 在 T5 執行 EXEC 時,Redis 會發現 k1 這個被監視的鍵已經被修改, 因此客戶端 A 的事務不會被執行,而是直接返回失敗。

7.4 錯誤處理

7.4.1 語法錯誤

語法錯誤指命令不存在或者命令參數的個數不對。跟在MULTI命令后執行了3個命令:一個是正確的命令,成功地加入事務隊列;其余兩個命令都有語法錯誤。而只要有一個命令有語法錯誤,執行EXEC命令后Redis就會直接返回錯誤,連語法正確的命令也不會執行。比如:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> GETT k1
(error) ERR unknown command `GETT`, with args beginning with: `k1`, 
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> GET k1
(nil)
127.0.0.1:6379> GET k2
(nil)

Redis 2.6.5之前的版本會忽略有語法錯誤的命令,然后執行事務中其他語法正確的命令。就此例而言,SET k1 v1會被執行,EXEC命令會返回一個結果:1) OK。

7.4.2 運行錯誤

運行錯誤指在命令執行時出現的錯誤,比如使用散列類型的命令操作集合類型的鍵,這種錯誤在實際執行之前Redis是無法發現的,所以在事務里這樣的命令是會被Redis接受并執行的。如果事務里的一條命令出現了運行錯誤,事務里其他的命令依然會繼續執行(包括出錯命令之后的命令),示例如下:

127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> SADD k1 v2
QUEUED
127.0.0.1:6379> GET k1
QUEUED
127.0.0.1:6379> SADD k2 1 2 3
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) "v1"
4) (integer) 3

7.5 Redis事務 V.S. MySQL事務

在傳統的關系式數據庫中,常常用ACID 性質來檢驗事務功能的安全性。
Redis 事務保證了其中的一致性(C)和隔離性(I),但并不保證原子性(A)和持久性(D)。

7.5.1 原子性(Atomicity)

單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增加任何維持原子性的機制,所以 Redis 事務的執行并不是原子性的。
如果一個事務隊列中的所有命令都被成功地執行,那么稱這個事務執行成功。
另一方面,如果 Redis 服務器進程在執行事務的過程中被停止 —— 比如接到 KILL 信號、宿主機器停機,等等,那么事務執行失敗。
當事務失敗時,Redis 也不會進行任何的重試或者回滾動作。

7.5.2 一致性(Consistency)

Redis 的一致性問題可以分為三部分來討論:運行錯誤、運行錯誤、Redis 進程被終結。

  • 語法錯誤:當Redis事務中出現語法錯誤時,會直接取消事務的執行,因此肯定是一致的。
  • 運行錯誤:當Redis事務中出現運行錯誤時,錯誤的命令并不會影響正常命令的執行,因此是一致的。
  • Redis進行被終結:
    如果 Redis 服務器進程在執行事務的過程中被其他進程終結,或者被管理員強制殺死,那么根據 Redis 所使用的持久化模式,可能有以下情況出現:
    • 內存模式:如果 Redis 沒有采取任何持久化機制,那么重啟之后的數據庫總是空白的,所以數據總是一致的。
    • RDB 模式:在執行事務時,Redis 不會中斷事務去執行保存 RDB 的工作,只有在事務執行之后,保存 RDB 的工作才有可能開始。所以當 RDB 模式下的 Redis 服務器進程在事務中途被殺死時,事務內執行的命令,不管成功了多少,都不會被保存到 RDB 文件里。恢復數據庫需要使用現有的 RDB 文件,而這個 RDB 文件的數據保存的是最近一次的數據庫快照(snapshot),所以它的數據可能不是最新的,但只要 RDB 文件本身沒有因為其他問題而出錯,那么還原后的數據庫就是一致的。
    • AOF 模式:因為保存 AOF 文件的工作在后臺線程進行,所以即使是在事務執行的中途,保存 AOF 文件的工作也可以繼續進行,因此,根據事務語句是否被寫入并保存到 AOF 文件,有以下兩種情況發生:
      1)如果事務語句未寫入到 AOF 文件,或 AOF 未被 SYNC 調用保存到磁盤,那么當進程被殺死之后,Redis 可以根據最近一次成功保存到磁盤的 AOF 文件來還原數據庫,只要 AOF 文件本身沒有因為其他問題而出錯,那么還原后的數據庫總是一致的,但其中的數據不一定是最新的。
      2)如果事務的部分語句被寫入到 AOF 文件,并且 AOF 文件被成功保存,那么不完整的事務執行信息就會遺留在 AOF 文件里,當重啟 Redis 時,程序會檢測到 AOF 文件并不完整,Redis 會退出,并報告錯誤。需要使用 redis-check-aof 工具將部分成功的事務命令移除之后,才能再次啟動服務器。還原之后的數據總是一致的,而且數據也是最新的(直到事務執行之前為止)。
7.5.3 隔離性(Isolation)

Redis 是單進程程序,并且它保證在執行事務時,不會對事務進行中斷,事務可以運行直到執行完所有事務隊列中的命令為止。因此,Redis 的事務是總是帶有隔離性的。

7.5.4 持久性(Durability)

因為事務不過是用隊列包裹起了一組 Redis 命令,并沒有提供任何額外的持久性功能,所以事務的持久性由 Redis 所使用的持久化模式決定:

  • 在單純的內存模式下,事務肯定是不持久的。
  • 在 RDB 模式下,服務器可能在事務執行之后、RDB 文件更新之前的這段時間失敗,所以 RDB 模式下的 Redis 事務也是不持久的。
  • 在 AOF 的“總是 SYNC ”模式下,事務的每條命令在執行成功之后,都會立即調用 fsyncfdatasync 將事務數據寫入到 AOF 文件。但是,這種保存是由后臺線程進行的,主線程不會阻塞直到保存成功,所以從命令執行成功到數據保存到硬盤之間,還是有一段非常小的間隔,所以這種模式下的事務也是不持久的。
    其他 AOF 模式也和“總是 SYNC ”模式類似,所以它們都是不持久的。

寫在最后

如果你覺得我寫的文章幫到了你,歡迎點贊、評論、分享、贊賞哦,你們的鼓勵是我不斷創作的動力~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。