Redis 有 5 個命令可讓用戶在不被打斷的狀況下對多個鍵執行操做, 它們分別是 WATCH
, MULTI
, EXEC
, UNWATCH
和 DISCARD
.redis
Redis 的基本事務須要用到 MULTI
, EXEC
命令, 這種事務可讓給一個客戶端在不被其餘客戶端打斷的狀況下執行多個命令.數據庫
在 Redis 裏面, 被 MULTI
, EXEC
命令會一個接一個的執行, 直到全部命令都執行完畢爲止. 當一個事務執行完畢以後, Redis 纔會處理其餘客戶端的命令.編程
值得注意的是, 在執行完MULTI
命令後, 仍是會繼續執行其餘客戶端的命令, 只要在執行EXEC
命令後, 纔不會去執行其餘客戶端的命令.Redis 的事務是不可嵌套的, 當客戶端已經處於事務狀態, 而客戶端又再向服務器發送
MULTI
時, 服務器只是簡單地向客戶端發送一個錯誤, 而後繼續等待其餘命令的入隊.MULTI
命令的發送不會形成整個事務失敗, 也不會修改事務隊列中已有的數據.數組
在執行完 MULTI
命令後, 而後添加想要在事務中執行的命令, 這一步只是添加, 添加到隊列中, 最後在執行 EXEC
命令開始執行事務.安全
重點服務器
在 Redis 中, 事務中的兩個保證:ide
EXEC
命令觸發事務中全部命令的執行, 所以, 當客戶端在事務上下文中失去與服務器的鏈接, 兩種狀況.EXEC
命令以前, 則不執行任何 commands;EXEC
命令以後, 則全部的 commands 都被執行.MULTI
命令的執行標記着事務的開始: 將客戶端的 REDIS_MULTI
選項打開, 讓客戶端從非事務狀態切換到事務狀態.函數
當客戶端處於非事務狀態下時, 全部發送給服務器端的命令都會當即被服務器執行.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
如下流程圖展現了這一行爲:
事務隊列是一個數組, 每一個數組項是都包含三個屬性:
- 要執行的命令 (cmd)
- 命令的參數 (argv)
- 參數的個數 (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
那麼程序將爲客戶端建立如下事務隊列:
前面說到, 當客戶端進入事務狀態以後, 客戶端發送的命令就會被放進事務隊列裏.
但其實並非全部的命令都會被放進事務隊列, 其中的例外就是 EXEC
、 DISCARD
、 MULTI
和 WATCH
這四個命令 —— 當這四個命令從客戶端發送到服務器時, 它們會像客戶端處於非事務狀態同樣, 直接被服務器執行:
若是客戶端正處於事務狀態, 那麼當 EXEC
命令執行時, 服務器根據客戶端所保存的事務隊列, 以先進先出 (FIFO) 的方式執行事務隊列中的命令: 最早入隊的命令最早執行, 而最後入隊的命令最後執行.
好比說, 對於如下事務隊列:
程序會首先執行 SET
命令, 而後執行 GET
命令, 再而後執行 SADD
命令, 最後執行 SMEMBERS
命令.
執行事務中的命令所得的結果會以 FIFO 的順序保存到一個回覆隊列中.
好比說, 對於上面給出的事務隊列, 程序將爲隊列中的命令建立以下回復隊列:
當事務隊列裏的全部命令被執行完以後, EXEC
命令會將回復隊列做爲本身的執行結果返回給客戶端, 客戶端從事務狀態返回到非事務狀態, 至此, 事務執行完畢.
不管在事務狀態下, 仍是在非事務狀態下, Redis 命令都由同一個函數執行, 因此它們共享不少服務器的通常設置, 好比 AOF 的配置、RDB 的配置, 以及內存限制, 等等.
不過事務中的命令和普通命令在執行上仍是有一點區別的, 其中最重要的兩點是:
EXEC
命令的結果返回給客戶端.事實上 Redis 命令在事務執行時可能會失敗, 但仍會繼續執行剩餘命令而不是 Rollback (事務回滾). 若是你使用過關係數據庫, 這種狀況可能會讓你感到很奇怪. 然而針對這種狀況具有很好的解釋:
Redis 命令可能會執行失敗, 僅僅是因爲錯誤的語法被調用 (命令排隊時檢測不出來的錯誤), 或者使用錯誤的數據類型操做某個 Key.
這意味着, 實際上失敗的命令都是編程錯誤形成的, 都是開發中可以被檢測出來的, 生產環境中不該該存在. (這番話, 完全甩鍋, 「都是大家本身編程錯誤, 與咱們無關」.)
因爲沒必要支持 Rollback, Redis 內部簡潔而且更加高效.
事務期間, 可能會遇到兩種命令錯誤:
在調用 EXEC 命令以前出現錯誤 (COMMAND 排隊失敗)
maxmemory
指令作了內存限制).客戶端會在 EXEC
調用以前檢測第一種錯誤. 經過檢查排隊命令的狀態回覆 (注意: 這裏是指排隊的狀態回覆, 而不是執行結果), 若是命令使用 QUEUED
進行響應, 則它已正確排隊, 不然 Redis 將返回錯誤. 若是排隊命令時發生錯誤, 大多數客戶端將停止該事務並清除命令隊列. 然而:
EXEC
命令調用後, 客戶端會執行命令的子集 (成功排隊的命令) 而忽略以前的錯誤.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
命令用於在事務開始以前監視任意數量的鍵: 當調用 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 鍵, 因此當客戶端執行事務時, 事務不會被執行.
在每一個表明數據庫的 redis.h/redisDb
結構類型中, 都保存了一個 watched_keys
字典, 字典的鍵是這個數據庫被監視的鍵, 而字典的值則是一個鏈表, 鏈表中保存了全部監視這個鍵的客戶端.
好比說, 如下字典就展現了一個 watched_keys
字典的例子:
其中, 鍵 key1
正在被 client2 、 client5 和 client1 三個客戶端監視, 其餘一些鍵也分別被其餘別的客戶端監視着.
WATCH
命令的做用, 就是將當前客戶端和要監視的鍵在 watched_keys
中進行關聯.
舉個例子, 若是當前客戶端爲 client10086, 那麼當客戶端執行 WATCH key1 key2
時, 前面展現的 watched_keys
將被修改爲這個樣子:
經過 watched_keys
字典, 若是程序想檢查某個鍵是否被監視, 那麼它只要檢查字典中是否存在這個鍵便可; 若是程序要獲取監視某個鍵的全部客戶端, 那麼只要取出鍵的值 (一個鏈表), 而後對鏈表進行遍歷便可.
在任何對數據庫鍵空間 (key space) 進行修改的命令成功執行以後 (好比 FLUSHDB
、 SET
、 DEL
、 LPUSH
、 SADD
、 ZREM
, 諸如此類), multi.c/touchWatchedKey
函數都會被調用 —— 它檢查數據庫的 watched_keys
字典, 看是否有客戶端在監視已經被命令修改的鍵, 若是有的話, 程序將全部監視這個/這些被修改鍵的客戶端的 REDIS_DIRTY_CAS
選項打開:
當客戶端發送 EXEC
命令、觸發事務執行時, 服務器會對客戶端的狀態進行檢查:
REDIS_DIRTY_CAS
選項已經被打開, 那麼說明被客戶端監視的鍵至少有一個已經被修改了, 事務的安全性已經被破壞. 服務器會放棄執行這個事務, 直接向客戶端返回空回覆, 表示事務執行失敗.REDIS_DIRTY_CAS
選項沒有被打開, 那麼說明全部監視鍵都安全, 服務器正式執行事務.舉個例子,假設數據庫的 watched_keys
字典以下圖所示:
若是某個客戶端對 key1 進行了修改 (好比執行 DEL key1
) 那麼全部監視 key1 的客戶端, 包括 client2 、 client5 和 client1 的 REDIS_DIRTY_CAS
選項都會被打開, 當客戶端 client2 、 client5 和 client1 執行 EXEC 的時候, 它們的事務都會以失敗了結.
值得注意的是, 當一個客戶端結束它的事務時, 不管事務是成功執行, 仍是失敗,
watched_keys
字典中和這個客戶端相關的資料都會被清除.