Redis 經過 MULTI 、 DISCARD 、 EXEC 和 WATCH 四個命令來實現事務功能, 本章首先討論使用 MULTI 、 DISCARD 和 EXEC 三個命令實現的通常事務, 而後再來討論帶有 WATCH 的事務的實現。
由於事務的安全性也很是重要, 因此本章最後經過常見的 ACID 性質對 Redis 事務的安全性進行了說明。html
事務提供了一種「將多個命令打包, 而後一次性、按順序地執行」的機制, 而且事務在執行的期間不會主動中斷 —— 服務器在執行完事務中的全部命令以後, 纔會繼續處理其餘客戶端的其餘命令。
如下是一個事務的例子, 它先以 MULTI 開始一個事務, 而後將多個命令入隊到事務中, 最後由 EXEC 命令觸發事務, 一併執行事務中的全部命令:java
redis 127.0.0.1:6379> multi OK redis 127.0.0.1:6379> set msg 'hello world' QUEUED redis 127.0.0.1:6379> get msg QUEUED redis 127.0.0.1:6379> sadd tag 'java' 'c++' 'C#' QUEUED redis 127.0.0.1:6379> smembers tag QUEUED redis 127.0.0.1:6379> exec 1) OK 2) "hello world" 3) (integer) 3 4) 1) "c++" 2) "C#" 3) "java" redis 127.0.0.1:6379>
一個事務從開始到執行會經歷如下三個階段:
1.開始事務。
2.命令入隊。
3.執行事務。
下文將分別介紹事務的這三個階段。c++
MULTI 命令的執行標記着事務的開始:redis
redis 127.0.0.1:6379> multi OK
這個命令惟一作的就是, 將客戶端的 REDIS_MULTI 選項打開, 讓客戶端從非事務狀態切換到事務狀態。數據庫
當客戶端處於非事務狀態下時, 全部發送給服務器端的命令都會當即被服務器執行:數組
redis 127.0.0.1:6379> set msg 'hello world' OK redis 127.0.0.1:6379> get msg "hello world"
可是, 當客戶端進入事務狀態以後, 服務器在收到來自客戶端的命令時, 不會當即執行命令, 而是將這些命令所有放進一個事務隊列裏, 而後返回 QUEUED , 表示命令已入隊:安全
redis 127.0.0.1:6379> set msg 'hello world' QUEUED redis 127.0.0.1:6379> get msg QUEUED
如下流程圖展現了這一行爲:服務器
事務隊列是一個數組, 每一個數組項是都包含三個屬性:
1.要執行的命令(cmd)。
2.命令的參數(argv)。
3.參數的個數(argc)。
舉個例子, 若是客戶端執行如下命令:app
redis 127.0.0.1:6379> multi OK redis 127.0.0.1:6379> set msg 'hello world' QUEUED redis 127.0.0.1:6379> get msg QUEUED redis 127.0.0.1:6379> sadd tag 'java' 'c++' 'C#' QUEUED redis 127.0.0.1:6379> smembers tag QUEUED
那麼程序將爲客戶端建立如下事務隊列:async
數字索引 | cmd | argv | argc |
---|---|---|---|
0 | set | ["msg","hello world"] | 2 |
1 | get | ["msg"] | 1 |
2 | sadd | ["tag","java","c++","C#"] | 4 |
3 | smembers | ["tag"] | 1 |
前面說到, 當客戶端進入事務狀態以後, 客戶端發送的命令就會被放進事務隊列裏。
但其實並非全部的命令都會被放進事務隊列, 其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 這四個命令 —— 當這四個命令從客戶端發送到服務器時, 它們會像客戶端處於非事務狀態同樣, 直接被服務器執行:
若是客戶端正處於事務狀態, 那麼當 EXEC 命令執行時, 服務器根據客戶端所保存的事務隊列, 以先進先出(FIFO)的方式執行事務隊列中的命令: 最早入隊的命令最早執行, 而最後入隊的命令最後執行。
好比說,對於如下事務隊列:
數字索引 | cmd | argv | argc |
---|---|---|---|
0 | set | ["msg","hello world"] | 2 |
1 | get | ["msg"] | 1 |
2 | sadd | ["tag","java","c++","C#"] | 4 |
3 | smembers | ["tag"] | 1 |
程序會首先執行 SET 命令, 而後執行 GET 命令, 再而後執行 SADD 命令, 最後執行 SMEMBERS 命令。
執行事務中的命令所得的結果會以 FIFO 的順序保存到一個回覆隊列中。
好比說,對於上面給出的事務隊列,程序將爲隊列中的命令建立以下回復隊列:
數字索引 | 回覆類型 | 回覆內容 |
---|---|---|
0 | status code reply | OK |
1 | bulk reply | "hello world" |
2 | integer reply | 3 |
3 | multi-bulk reply | ["java","c++","C#"] |
當事務隊列裏的全部命令被執行完以後, EXEC 命令會將回復隊列做爲本身的執行結果返回給客戶端, 客戶端從事務狀態返回到非事務狀態, 至此, 事務執行完畢。
事務的整個執行過程能夠用如下僞代碼表示:
def execute_transaction(): # 建立空白的回覆隊列 reply_queue = [] # 取出事務隊列裏的全部命令、參數和參數數量 for cmd, argv, argc in client.transaction_queue: # 執行命令,並取得命令的返回值 reply = execute_redis_command(cmd, argv, argc) # 將返回值追加到回覆隊列末尾 reply_queue.append(reply) # 清除客戶端的事務狀態 clear_transaction_state(client) # 清空事務隊列 clear_transaction_queue(client) # 將事務的執行結果返回給客戶端 send_reply_to_client(client, reply_queue)
不管在事務狀態下, 仍是在非事務狀態下, Redis 命令都由同一個函數執行, 因此它們共享不少服務器的通常設置, 好比 AOF 的配置、RDB 的配置,以及內存限制,等等。
不過事務中的命令和普通命令在執行上仍是有一點區別的,其中最重要的兩點是:
1.非事務狀態下的命令以單個命令爲單位執行,前一個命令和後一個命令的客戶端不必定是同一個;
而事務狀態則是以一個事務爲單位,執行事務隊列中的全部命令:除非當前事務執行完畢,不然服務器不會中斷事務,也不會執行其餘客戶端的其餘命令。
2.在非事務狀態下,執行命令所得的結果會當即被返回給客戶端;
而事務則是將全部命令的結果集合到回覆隊列,再做爲 EXEC 命令的結果返回給客戶端。
除了 EXEC 以外, 服務器在客戶端處於事務狀態時, 不加入到事務隊列而直接執行的另外三個命令是 DISCARD 、 MULTI 和 WATCH 。
DISCARD 命令用於取消一個事務, 它清空客戶端的整個事務隊列, 而後將客戶端從事務狀態調整回非事務狀態, 最後返回字符串 OK
給客戶端, 說明事務已被取消。
Redis 的事務是不可嵌套的, 當客戶端已經處於事務狀態, 而客戶端又再向服務器發送 MULTI 時, 服務器只是簡單地向客戶端發送一個錯誤, 而後繼續等待其餘命令的入隊。 MULTI 命令的發送不會形成整個事務失敗, 也不會修改事務隊列中已有的數據。
WATCH 只能在客戶端進入事務狀態以前執行, 在事務狀態下發送 WATCH 命令會引起一個錯誤, 但它不會形成整個事務失敗, 也不會修改事務隊列中已有的數據(和前面處理 MULTI 的狀況同樣)。
WATCH 命令用於在事務開始以前監視任意數量的鍵: 當調用 EXEC 命令執行事務時, 若是任意一個被監視的鍵已經被其餘客戶端修改了, 那麼整個事務再也不執行, 直接返回失敗。
如下示例展現了一個執行失敗的事務例子:
redis 127.0.0.1:6379> watch name OK redis 127.0.0.1:6379> multi OK redis 127.0.0.1:6379> set name 'cheng' QUEUED redis 127.0.0.1:6379> exec (nil) redis 127.0.0.1:6379>
如下執行序列展現了上面的例子是如何失敗的:
時間 | 客戶端A | 客戶端B |
---|---|---|
t1 | watch name | |
t2 | multi | |
t3 | set name 'cheng' | |
t4 | set name 'zhao' | |
t5 | exec |
在時間 T4 ,客戶端 B 修改了 name 鍵的值, 當客戶端 A 在 T5 執行 EXEC 時,Redis 會發現 name 這個被監視的鍵已經被修改, 所以客戶端 A 的事務不會被執行,而是直接返回失敗。
下文就來介紹 WATCH 的實現機制,而且看看事務系統是如何檢查某個被監視的鍵是否被修改,從而保證事務的安全性的。
在每一個表明數據庫的 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 命令、觸發事務執行時, 服務器會對客戶端的狀態進行檢查:
def check_safety_before_execute_trasaction(): if client.state & REDIS_DIRTY_CAS: # 安全性已破壞,清除事務狀態 clear_transaction_state(client) # 清空事務隊列 clear_transaction_queue(client) # 返回空回覆給客戶端 send_empty_reply(client) else: # 安全性無缺,執行事務 execute_transaction()
舉個例子,假設數據庫的 watched_keys 字典以下圖所示:
若是某個客戶端對 key1 進行了修改(好比執行 DEL key1 ), 那麼全部監視 key1 的客戶端, 包括 client2 、 client5 和 client1 的 REDIS_DIRTY_CAS 選項都會被打開, 當客戶端 client2 、 client5 和 client1 執行 EXEC 的時候, 它們的事務都會以失敗了結。
最後,當一個客戶端結束它的事務時,不管事務是成功執行,仍是失敗, watched_keys 字典中和這個客戶端相關的資料都會被清除。
在傳統的關係式數據庫中,經常用 ACID 性質來檢驗事務功能的安全性。
Redis 事務保證了其中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)。
如下四小節是關於這四個性質的詳細討論。
單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增長任何維持原子性的機制,因此 Redis 事務的執行並非原子性的。若是一個事務隊列中的全部命令都被成功地執行,那麼稱這個事務執行成功。
另外一方面,若是 Redis 服務器進程在執行事務的過程當中被中止 —— 好比接到 KILL 信號、宿主機器停機,等等,那麼事務執行失敗。當事務失敗時,Redis 也不會進行任何的重試或者回滾動做。
Redis 的一致性問題能夠分爲三部分來討論:入隊錯誤、執行錯誤、Redis 進程被終結。
入隊錯誤
在命令入隊的過程當中,若是客戶端向服務器發送了錯誤的命令,好比命令的參數數量不對,等等, 那麼服務器將向客戶端返回一個出錯信息, 而且將客戶端的事務狀態設爲 REDIS_DIRTY_EXEC 。
當客戶端執行 EXEC 命令時, Redis 會拒絕執行狀態爲 REDIS_DIRTY_EXEC的事務, 並返回失敗信息。
redis 127.0.0.1:6379> MULTI OK redis 127.0.0.1:6379> set key (error) ERR wrong number of arguments for 'set' command redis 127.0.0.1:6379> EXISTS key QUEUED redis 127.0.0.1:6379> EXEC (error) EXECABORT Transaction discarded because of previous errors.
所以,帶有不正確入隊命令的事務不會被執行,也不會影響數據庫的一致性。
執行錯誤
若是命令在事務執行的過程當中發生錯誤,好比說,對一個不一樣類型的 key 執行了錯誤的操做, 那麼 Redis 只會將錯誤包含在事務的結果中, 這不會引發事務中斷或整個失敗,不會影響已執行事務命令的結果,也不會影響後面要執行的事務命令, 因此它對事務的一致性也沒有影響。
Redis 進程被終結
若是 Redis 服務器進程在執行事務的過程當中被其餘進程終結,或者被管理員強制殺死,那麼根據 Redis 所使用的持久化模式,可能有如下狀況出現:
Redis 是單進程程序,而且它保證在執行事務時,不會對事務進行中斷,事務能夠運行直到執行完全部事務隊列中的命令爲止。所以,Redis 的事務是老是帶有隔離性的。
由於事務不過是用隊列包裹起了一組 Redis 命令,並無提供任何額外的持久性功能,因此事務的持久性由 Redis 所使用的持久化模式決定: