Redis 經過 MULTI 、 DISCARD 、 EXEC 和 WATCH 四個命令來實現事務功能, 本章首先討論使用 MULTI 、 DISCARD 和 EXEC 三個命令實現的通常事務, 而後再來討論帶有 WATCH 的事務的實現。html
由於事務的安全性也很是重要, 因此本章最後經過常見的 ACID 性質對 Redis 事務的安全性進行了說明。node
事務提供了一種「將多個命令打包, 而後一次性、按順序地執行」的機制, 而且事務在執行的期間不會主動中斷 —— 服務器在執行完事務中的全部命令以後, 纔會繼續處理其餘客戶端的其餘命令。redis
如下是一個事務的例子, 它先以 MULTI 開始一個事務, 而後將多個命令入隊到事務中, 最後由 EXEC 命令觸發事務, 一併執行事務中的全部命令:數據庫
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 redis> EXEC 1) OK 2) "Mastering C++ in 21 days" 3) (integer) 3 4) 1) "Mastering Series" 2) "C++" 3) "Programming"
一個事務從開始到執行會經歷如下三個階段:數組
下文將分別介紹事務的這三個階段。安全
MULTI 命令的執行標記着事務的開始:服務器
redis> MULTI OK
這個命令惟一作的就是, 將客戶端的 REDIS_MULTI
選項打開, 讓客戶端從非事務狀態切換到事務狀態。app
當客戶端處於非事務狀態下時, 全部發送給服務器端的命令都會當即被服務器執行:async
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
如下流程圖展現了這一行爲:
事務隊列是一個數組, 每一個數組項是都包含三個屬性:
舉個例子, 若是客戶端執行如下命令:
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
那麼程序將爲客戶端建立如下事務隊列:
數組索引 | cmd | argv | argc |
---|---|---|---|
0 |
SET |
["book-name", "Mastering C++ in 21 days"] |
2 |
1 |
GET |
["book-name"] |
1 |
2 |
SADD |
["tag", "C++", "Programming", "Mastering Series"] |
4 |
3 |
SMEMBERS |
["tag"] |
1 |
前面說到, 當客戶端進入事務狀態以後, 客戶端發送的命令就會被放進事務隊列裏。
但其實並非全部的命令都會被放進事務隊列, 其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 這四個命令 —— 當這四個命令從客戶端發送到服務器時, 它們會像客戶端處於非事務狀態同樣, 直接被服務器執行:
若是客戶端正處於事務狀態, 那麼當 EXEC 命令執行時, 服務器根據客戶端所保存的事務隊列, 以先進先出(FIFO)的方式執行事務隊列中的命令: 最早入隊的命令最早執行, 而最後入隊的命令最後執行。
好比說,對於如下事務隊列:
數組索引 | cmd | argv | argc |
---|---|---|---|
0 |
SET |
["book-name", "Mastering C++ in 21 days"] |
2 |
1 |
GET |
["book-name"] |
1 |
2 |
SADD |
["tag", "C++", "Programming", "Mastering Series"] |
4 |
3 |
SMEMBERS |
["tag"] |
1 |
程序會首先執行 SET 命令, 而後執行 GET 命令, 再而後執行 SADD 命令, 最後執行 SMEMBERS 命令。
執行事務中的命令所得的結果會以 FIFO 的順序保存到一個回覆隊列中。
好比說,對於上面給出的事務隊列,程序將爲隊列中的命令建立以下回復隊列:
數組索引 | 回覆類型 | 回覆內容 |
---|---|---|
0 |
status code reply | OK |
1 |
bulk reply | "Mastering C++ in 21 days" |
2 |
integer reply | 3 |
3 |
multi-bulk reply | ["Mastering Series", "C++", "Programming"] |
當事務隊列裏的全部命令被執行完以後, 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 的配置,以及內存限制,等等。
不過事務中的命令和普通命令在執行上仍是有一點區別的,其中最重要的兩點是:
非事務狀態下的命令以單個命令爲單位執行,前一個命令和後一個命令的客戶端不必定是同一個;
而事務狀態則是以一個事務爲單位,執行事務隊列中的全部命令:除非當前事務執行完畢,不然服務器不會中斷事務,也不會執行其餘客戶端的其餘命令。
在非事務狀態下,執行命令所得的結果會當即被返回給客戶端;
而事務則是將全部命令的結果集合到回覆隊列,再做爲 EXEC 命令的結果返回給客戶端。
除了 EXEC 以外, 服務器在客戶端處於事務狀態時, 不加入到事務隊列而直接執行的另外三個命令是 DISCARD 、 MULTI 和 WATCH 。
DISCARD 命令用於取消一個事務, 它清空客戶端的整個事務隊列, 而後將客戶端從事務狀態調整回非事務狀態, 最後返回字符串 OK
給客戶端, 說明事務已被取消。
Redis 的事務是不可嵌套的, 當客戶端已經處於事務狀態, 而客戶端又再向服務器發送 MULTI 時, 服務器只是簡單地向客戶端發送一個錯誤, 而後繼續等待其餘命令的入隊。 MULTI 命令的發送不會形成整個事務失敗, 也不會修改事務隊列中已有的數據。
WATCH 只能在客戶端進入事務狀態以前執行, 在事務狀態下發送 WATCH 命令會引起一個錯誤, 但它不會形成整個事務失敗, 也不會修改事務隊列中已有的數據(和前面處理 MULTI 的狀況同樣)。
WATCH 命令用於在事務開始以前監視任意數量的鍵: 當調用 EXEC 命令執行事務時, 若是任意一個被監視的鍵已經被其餘客戶端修改了, 那麼整個事務再也不執行, 直接返回失敗。
如下示例展現了一個執行失敗的事務例子:
redis> WATCH name OK redis> MULTI OK redis> SET name peter QUEUED redis> EXEC (nil)
如下執行序列展現了上面的例子是如何失敗的:
時間 | 客戶端 A | 客戶端 B |
---|---|---|
T1 | WATCH name |
|
T2 | MULTI |
|
T3 | SET name peter |
|
T4 | SET name john |
|
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 命令、觸發事務執行時, 服務器會對客戶端的狀態進行檢查:
REDIS_DIRTY_CAS
選項已經被打開,那麼說明被客戶端監視的鍵至少有一個已經被修改了,事務的安全性已經被破壞。服務器會放棄執行這個事務,直接向客戶端返回空回覆,表示事務執行失敗。REDIS_DIRTY_CAS
選項沒有被打開,那麼說明全部監視鍵都安全,服務器正式執行事務。能夠用一段僞代碼來表示這個檢查:
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
字典中和這個客戶端相關的資料都會被清除。
勘誤:Redis 的事務是保證原子性的,本節的內容將原子性和回滾功能混淆了,等待修復中。 —— 2013.6.23
在傳統的關係式數據庫中,經常用 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 沒有采起任何持久化機制,那麼重啓以後的數據庫老是空白的,因此數據老是一致的。
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 工具將部分紅功的事務命令移除以後,才能再次啓動服務器。還原以後的數據老是一致的,並且數據也是最新的(直到事務執行以前爲止)。
Redis 是單進程程序,而且它保證在執行事務時,不會對事務進行中斷,事務能夠運行直到執行完全部事務隊列中的命令爲止。所以,Redis 的事務是老是帶有隔離性的。
由於事務不過是用隊列包裹起了一組 Redis 命令,並無提供任何額外的持久性功能,因此事務的持久性由 Redis 所使用的持久化模式決定:
在單純的內存模式下,事務確定是不持久的。
在 RDB 模式下,服務器可能在事務執行以後、RDB 文件更新以前的這段時間失敗,因此 RDB 模式下的 Redis 事務也是不持久的。
在 AOF 的「老是 SYNC 」模式下,事務的每條命令在執行成功以後,都會當即調用 fsync
或 fdatasync
將事務數據寫入到 AOF 文件。可是,這種保存是由後臺線程進行的,主線程不會阻塞直到保存成功,因此從命令執行成功到數據保存到硬盤之間,仍是有一段很是小的間隔,因此這種模式下的事務也是不持久的。
其餘 AOF 模式也和「老是 SYNC 」模式相似,因此它們都是不持久的。
WATCH
命令的事務會將客戶端和被監視的鍵在數據庫的 watched_keys
字典中進行關聯,當鍵被修改時,程序會將全部監視被修改鍵的客戶端的 REDIS_DIRTY_CAS
選項打開。REDIS_DIRTY_CAS
選項未被打開時,才能執行事務,不然事務直接返回失敗。