Redis 事務

基本的 Redis 事務

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

Redis 的基本事務須要用到 MULTI, EXEC 命令, 這種事務可讓給一個客戶端在不被其餘客戶端打斷的狀況下執行多個命令.數據庫

在 Redis 裏面, 被 MULTI, EXEC 命令會一個接一個的執行, 直到全部命令都執行完畢爲止. 當一個事務執行完畢以後, Redis 纔會處理其餘客戶端的命令.編程

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

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

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

重點服務器

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

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

開始事務

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

clipboard.png

命令入隊

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

redis> SET msg "hello moto"
OK

redis> GET msg
"hello moto"

可是, 當客戶端進入事務狀態以後, 服務器在收到來自客戶端的命令時, 不會當即執行命令, 而是將這些命令所有放進一個事務隊列裏, 而後返回 QUEUED, 表示命令已入隊:3d

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 字典中和這個客戶端相關的資料都會被清除.
相關文章
相關標籤/搜索