Redis 事務

基本的 Redis 事務

Redis 有 5 個命令可以讓用戶在不被打斷的情況下對多個鍵執行操作, 它們分別是 WATCH, MULTI, EXEC, UNWATCHDISCARD.

Redis 的基本事務需要用到 MULTI, EXEC 命令, 這種事務可以讓給一個客戶端在不被其他客戶端打斷的情況下執行多個命令.

在 Redis 裏面, 被 MULTI, EXEC 命令會一個接一個的執行, 直到所有命令都執行完畢爲止. 當一個事務執行完畢之後, Redis 纔會處理其他客戶端的命令.

值得注意的是, 在執行完 MULTI 命令後, 還是會繼續執行其他客戶端的命令, 只要在執行 EXEC 命令後, 纔不會去執行其他客戶端的命令.

Redis 的事務是不可嵌套的, 當客戶端已經處於事務狀態, 而客戶端又再向服務器發送 MULTI 時, 服務器只是簡單地向客戶端發送一個錯誤, 然後繼續等待其他命令的入隊. MULTI 命令的發送不會造成整個事務失敗, 也不會修改事務隊列中已有的數據.

在執行完 MULTI 命令後, 然後添加想要在事務中執行的命令, 這一步只是添加, 添加到隊列中, 最後在執行 EXEC 命令開始執行事務.

重點

在 Redis 中, 事務中的兩個保證:

  • 事務中的所有命令都會被序列化並按順序執行. 在執行 Redis 事務的過程中, 不會出現執行另一個客戶端的請求. 這保證 命令隊列 作爲一個單獨的原子操作被執行.
  • 隊列中的命令要麼全部被處理, 要麼全部被忽略. EXEC 命令觸發事務中所有命令的執行, 因此, 當客戶端在事務上下文中失去與服務器的連接, 兩種情況.
    • 如果發生在調用 EXEC 命令之前, 則不執行任何 commands;
    • 如果發生在調用 EXEC 命令之後, 則所有的 commands 都被執行.

開始事務

MULTI 命令的執行標記着事務的開始: 將客戶端的 REDIS_MULTI 選項打開, 讓客戶端從非事務狀態切換到事務狀態.

clipboard.png

命令入隊

當客戶端處於非事務狀態下時, 所有發送給服務器端的命令都會立即被服務器執行.

redis> SET msg "hello moto"
OK

redis> GET msg
"hello moto"

但是, 當客戶端進入事務狀態之後, 服務器在收到來自客戶端的命令時, 不會立即執行命令, 而是將這些命令全部放進一個事務隊列裏, 然後返回 QUEUED, 表示命令已入隊:

redis> MULTI
OK

redis> SET msg "hello moto"
QUEUED

redis> GET msg
QUEUED

以下流程圖展示了這一行爲:

clipboard.png

事務隊列是一個數組, 每個數組項是都包含三個屬性:

  1. 要執行的命令 (cmd)
  2. 命令的參數 (argv)
  3. 參數的個數 (argc)

舉個例子, 如果客戶端執行以下命令:

redis> MULTI
OK

redis> SET book-name "Mastering C++ in 21 days"
QUEUED

redis> GET book-name
QUEUED

redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED

redis> SMEMBERS tag
QUEUED

那麼程序將爲客戶端創建以下事務隊列:

clipboard.png

執行事務

前面說到, 當客戶端進入事務狀態之後, 客戶端發送的命令就會被放進事務隊列裏.

但其實並不是所有的命令都會被放進事務隊列, 其中的例外就是 EXECDISCARDMULTIWATCH 這四個命令 —— 當這四個命令從客戶端發送到服務器時, 它們會像客戶端處於非事務狀態一樣, 直接被服務器執行:

clipboard.png

如果客戶端正處於事務狀態, 那麼當 EXEC 命令執行時, 服務器根據客戶端所保存的事務隊列, 以先進先出 (FIFO) 的方式執行事務隊列中的命令: 最先入隊的命令最先執行, 而最後入隊的命令最後執行.

比如說, 對於以下事務隊列:

clipboard.png

程序會首先執行 SET 命令, 然後執行 GET 命令, 再然後執行 SADD 命令, 最後執行 SMEMBERS 命令.

執行事務中的命令所得的結果會以 FIFO 的順序保存到一個回覆隊列中.

比如說, 對於上面給出的事務隊列, 程序將爲隊列中的命令創建如下回復隊列:

clipboard.png

當事務隊列裏的所有命令被執行完之後, EXEC 命令會將回復隊列作爲自己的執行結果返回給客戶端, 客戶端從事務狀態返回到非事務狀態, 至此, 事務執行完畢.

在事務和非事務狀態下執行命令

無論在事務狀態下, 還是在非事務狀態下, Redis 命令都由同一個函數執行, 所以它們共享很多服務器的一般設置, 比如 AOF 的配置、RDB 的配置, 以及內存限制, 等等.

不過事務中的命令和普通命令在執行上還是有一點區別的, 其中最重要的兩點是:

  • 非事務狀態下的命令以單個命令爲單位執行, 前一個命令和後一個命令的客戶端不一定是同一個;
    而事務狀態則是以一個事務爲單位, 執行事務隊列中的所有命令: 除非當前事務執行完畢, 否則服務器不會中斷事務, 也不會執行其他客戶端的其他命令.
  • 在非事務狀態下, 執行命令所得的結果會立即被返回給客戶端;
    而事務則是將所有命令的結果集合到回覆隊列, 再作爲 EXEC 命令的結果返回給客戶端.

Redis 事務不支持 Rollback

事實上 Redis 命令在事務執行時可能會失敗, 但仍會繼續執行剩餘命令而不是 Rollback (事務回滾). 如果你使用過關係數據庫, 這種情況可能會讓你感到很奇怪. 然而針對這種情況具備很好的解釋:

Redis 命令可能會執行失敗, 僅僅是由於錯誤的語法被調用 (命令排隊時檢測不出來的錯誤), 或者使用錯誤的數據類型操作某個 Key.

這意味着, 實際上失敗的命令都是編程錯誤造成的, 都是開發中能夠被檢測出來的, 生產環境中不應該存在. (這番話, 徹底甩鍋, “都是你們自己編程錯誤, 與我們無關”.)

由於不必支持 Rollback, Redis 內部簡潔並且更加高效.

事務中的錯誤

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

在調用 EXEC 命令之前出現錯誤 (COMMAND 排隊失敗)

  • 例如, 命令可能存在語法錯誤 (參數數量錯誤, 錯誤的命令名稱);
  • 或者可能存在某些關鍵條件, 如內存不足的情況 (如果服務器使用 maxmemory 指令做了內存限制).

客戶端會在 EXEC 調用之前檢測第一種錯誤. 通過檢查排隊命令的狀態回覆 (注意: 這裏是指排隊的狀態回覆, 而不是執行結果), 如果命令使用 QUEUED 進行響應, 則它已正確排隊, 否則 Redis 將返回錯誤. 如果排隊命令時發生錯誤, 大多數客戶端將中止該事務並清除命令隊列. 然而:

  • 在 Redis 2.6.5 之前, 這種情況下, 在 EXEC 命令調用後, 客戶端會執行命令的子集 (成功排隊的命令) 而忽略之前的錯誤.
  • 從 Redis 2.6.5 開始, 服務端會記住在累積命令期間發生的錯誤, 當 EXEC 命令調用時, 將拒絕執行事務, 並返回這些錯誤, 同時自動清除命令隊列.
>MULTI
+OK
>INCR a b c
-ERR wrong number of arguments for 'incr' command
這是由於 INCR 命令的語法錯誤, 將在調用 EXEC 之前被檢測出來, 並終止事務.

在調用 EXEC 命令之後出現錯誤

例如, 使用錯誤的值對某個 key 執行操作 (如針對 String 值調用 List 操作).

EXEC 命令執行之後發生的錯誤並不會被特殊對待: 即使事務中的某些命令執行失敗, 其他命令仍會被正常執行.

>MULTI
+OK
>SET a 3
+QUEUED
>LPOP a
+QUEUED
>EXEC
*2
+OK
-ERR Operation against a key holding the wrong kind of value

EXEC 返回一個包含兩個元素的字符串數組, 一個元素是OK, 另一個是-ERR…….

能否將錯誤合理的反饋給用戶這取決於客戶端 library (如: Spring-data-redis.redisTemplate) 的自身實現.

需要注意的是, 即使命令失敗, 隊列中的所有其他命令也會被處理, 即 Redis 不會停止命令的處理.

清除命令隊列

DISCARD 被用來中止事務. 事務中的所有命令將不會被執行, 連接將恢復正常狀態.

> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"

WATCH

WATCH 命令用於在事務開始之前監視任意數量的鍵: 當調用 EXEC 命令執行事務時, 如果任意一個被監視的鍵已經被其他客戶端修改了(或刪除), 那麼整個事務不再執行, 直接返回失敗.

值的注意的是, WATCH 只能在客戶端進入事務狀態之前執行, 在事務狀態下發送 WATCH 命令會引發一個錯誤, 但它不會造成整個事務失敗, 也不會修改事務隊列中已有的數據.
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> WATCH key
(error) ERR WATCH inside MULTI is not allowed

client1 正常情況

127.0.0.1:6379> GET key
"2"
127.0.0.1:6379> WATCH key
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key 3
QUEUED
127.0.0.1:6379> GET key
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "3"
127.0.0.1:6379> 

client1 監視了 key 這個鍵, 然後執行事務. 事務正常執行, 並沒有返回 (nil).

因爲並沒有在 client2(或其他客戶端) 中來修改 key 這個鍵. 所以事務可以正常執行.


在 client2 中修改了 key 鍵

時間 client1 client2
T1 WATCH key
T2 MULTI
T3 SET key 4
T4 SET key 1
T5 EXEC

執行 EXEC 後返回 (nil), 說明任務執行失敗了.

原因是在 T4(時間) 的時候執行了 SET key 1, 來修改了 client1 正在監視的 key 鍵, 所以當客戶端執行事務時, 事務不會被執行.

WATCH 命令的實現

在每個代表數據庫的 redis.h/redisDb 結構類型中, 都保存了一個 watched_keys 字典, 字典的鍵是這個數據庫被監視的鍵, 而字典的值則是一個鏈表, 鏈表中保存了所有監視這個鍵的客戶端.

比如說, 以下字典就展示了一個 watched_keys 字典的例子:

clipboard.png

其中, 鍵 key1 正在被 client2 、 client5 和 client1 三個客戶端監視, 其他一些鍵也分別被其他別的客戶端監視着.

WATCH 命令的作用, 就是將當前客戶端和要監視的鍵在 watched_keys 中進行關聯.

舉個例子, 如果當前客戶端爲 client10086, 那麼當客戶端執行 WATCH key1 key2 時, 前面展示的 watched_keys 將被修改成這個樣子:

clipboard.png

通過 watched_keys 字典, 如果程序想檢查某個鍵是否被監視, 那麼它只要檢查字典中是否存在這個鍵即可; 如果程序要獲取監視某個鍵的所有客戶端, 那麼只要取出鍵的值 (一個鏈表), 然後對鏈表進行遍歷即可.

WATCH 的觸發

在任何對數據庫鍵空間 (key space) 進行修改的命令成功執行之後 (比如 FLUSHDBSETDELLPUSHSADDZREM, 諸如此類), multi.c/touchWatchedKey 函數都會被調用 —— 它檢查數據庫的 watched_keys 字典, 看是否有客戶端在監視已經被命令修改的鍵, 如果有的話, 程序將所有監視這個/這些被修改鍵的客戶端的 REDIS_DIRTY_CAS 選項打開:

clipboard.png

當客戶端發送 EXEC 命令、觸發事務執行時, 服務器會對客戶端的狀態進行檢查:

  • 如果客戶端的 REDIS_DIRTY_CAS 選項已經被打開, 那麼說明被客戶端監視的鍵至少有一個已經被修改了, 事務的安全性已經被破壞. 服務器會放棄執行這個事務, 直接向客戶端返回空回覆, 表示事務執行失敗.
  • 如果 REDIS_DIRTY_CAS 選項沒有被打開, 那麼說明所有監視鍵都安全, 服務器正式執行事務.

舉個例子,假設數據庫的 watched_keys 字典如下圖所示:

clipboard.png

如果某個客戶端對 key1 進行了修改 (比如執行 DEL key1) 那麼所有監視 key1 的客戶端, 包括 client2 、 client5 和 client1 的 REDIS_DIRTY_CAS 選項都會被打開, 當客戶端 client2 、 client5 和 client1 執行 EXEC 的時候, 它們的事務都會以失敗告終.

值得注意的是, 當一個客戶端結束它的事務時, 無論事務是成功執行, 還是失敗, watched_keys 字典中和這個客戶端相關的資料都會被清除.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章