Redis-事物

Redis 經過 MULTIDISCARDEXECWATCH 四個命令來實現事務功能, 本章首先討論使用 MULTIDISCARDEXEC 三個命令實現的通常事務, 而後再來討論帶有 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

執行事物

前面說到, 當客戶端進入事務狀態以後, 客戶端發送的命令就會被放進事務隊列裏。
但其實並非全部的命令都會被放進事務隊列, 其中的例外就是 EXECDISCARDMULTIWATCH 這四個命令 —— 當這四個命令從客戶端發送到服務器時, 它們會像客戶端處於非事務狀態同樣, 直接被服務器執行:

 


若是客戶端正處於事務狀態, 那麼當 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 命令的結果返回給客戶端。

事務狀態下的 DISCARD 、 MULTI 和 WATCH 命令

除了 EXEC 以外, 服務器在客戶端處於事務狀態時, 不加入到事務隊列而直接執行的另外三個命令是 DISCARDMULTIWATCH
DISCARD 命令用於取消一個事務, 它清空客戶端的整個事務隊列, 而後將客戶端從事務狀態調整回非事務狀態, 最後返回字符串 OK
給客戶端, 說明事務已被取消。
Redis 的事務是不可嵌套的, 當客戶端已經處於事務狀態, 而客戶端又再向服務器發送 MULTI 時, 服務器只是簡單地向客戶端發送一個錯誤, 而後繼續等待其餘命令的入隊。 MULTI 命令的發送不會形成整個事務失敗, 也不會修改事務隊列中已有的數據。
WATCH 只能在客戶端進入事務狀態以前執行, 在事務狀態下發送 WATCH 命令會引起一個錯誤, 但它不會形成整個事務失敗, 也不會修改事務隊列中已有的數據(和前面處理 MULTI 的狀況同樣)。

帶 WATCH 的事務

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 的實現機制,而且看看事務系統是如何檢查某個被監視的鍵是否被修改,從而保證事務的安全性的。

WATCH 命令的實現

在每一個表明數據庫的 redis.h/redisDb 結構類型中, 都保存了一個 watched_keys 字典, 字典的鍵是這個數據庫被監視的鍵, 而字典的值則是一個鏈表, 鏈表中保存了全部監視這個鍵的客戶端。
好比說,如下字典就展現了一個 watched_keys 字典的例子:

 

其中, 鍵 key1 正在被 client2 、 client5 和 client1 三個客戶端監視, 其餘一些鍵也分別被其餘別的客戶端監視着。
WATCH 命令的做用, 就是將當前客戶端和要監視的鍵在 watched_keys 中進行關聯。
舉個例子, 若是當前客戶端爲 client10086 , 那麼當客戶端執行 WATCH key1 key2 時, 前面展現的 watched_keys 將被修改爲這個樣子:

 

經過 watched_keys 字典, 若是程序想檢查某個鍵是否被監視, 那麼它只要檢查字典中是否存在這個鍵便可; 若是程序要獲取監視某個鍵的全部客戶端, 那麼只要取出鍵的值(一個鏈表), 而後對鏈表進行遍歷便可。

WATCH 的觸發

在任何對數據庫鍵空間(key space)進行修改的命令成功執行以後 (好比 FLUSHDBSETDELLPUSHSADDZREM ,諸如此類), 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 字典中和這個客戶端相關的資料都會被清除。

事務的 ACID 性質

在傳統的關係式數據庫中,經常用 ACID 性質來檢驗事務功能的安全性。
Redis 事務保證了其中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)。
如下四小節是關於這四個性質的詳細討論。

原子性(Atomicity)

單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增長任何維持原子性的機制,因此 Redis 事務的執行並非原子性的。若是一個事務隊列中的全部命令都被成功地執行,那麼稱這個事務執行成功。
另外一方面,若是 Redis 服務器進程在執行事務的過程當中被中止 —— 好比接到 KILL 信號、宿主機器停機,等等,那麼事務執行失敗。當事務失敗時,Redis 也不會進行任何的重試或者回滾動做。

一致性(Consistency)

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 沒有采起任何持久化機制,那麼重啓以後的數據庫老是空白的,因此數據老是一致的。
  • 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 工具將部分紅功的事務命令移除以後,才能再次啓動服務器。還原以後的數據老是一致的,並且數據也是最新的(直到事務執行以前爲止)。

隔離性(Isolation)

Redis 是單進程程序,而且它保證在執行事務時,不會對事務進行中斷,事務能夠運行直到執行完全部事務隊列中的命令爲止。所以,Redis 的事務是老是帶有隔離性的。

持久性(Durability)

由於事務不過是用隊列包裹起了一組 Redis 命令,並無提供任何額外的持久性功能,因此事務的持久性由 Redis 所使用的持久化模式決定:

  • 在單純的內存模式下,事務確定是不持久的。
  • 在 RDB 模式下,服務器可能在事務執行以後、RDB 文件更新以前的這段時間失敗,因此 RDB 模式下的 Redis 事務也是不持久的。
  • 在 AOF 的「老是 SYNC 」模式下,事務的每條命令在執行成功以後,都會當即調用 fsync 或 fdatasync 將事務數據寫入到 AOF 文件。可是,這種保存是由後臺線程進行的,主線程不會阻塞直到保存成功,因此從命令執行成功到數據保存到硬盤之間,仍是有一段很是小的間隔,因此這種模式下的事務也是不持久的。
    其餘 AOF 模式也和「老是 SYNC 」模式相似,因此它們都是不持久的。

總結

  • 事務提供了一種將多個命令打包,而後一次性、有序地執行的機制。
  • 事務在執行過程當中不會被中斷,全部事務命令執行完以後,事務才能結束。
  • 多個命令會被入隊到事務隊列中,而後按先進先出(FIFO)的順序執行。
  • 帶 WATCH 命令的事務會將客戶端和被監視的鍵在數據庫的 watched_keys 字典中進行關聯,當鍵被修改時,程序會將全部監視被修改鍵的客戶端的 REDIS_DIRTY_CAS 選項打開。
  • 只有在客戶端的 REDIS_DIRTY_CAS 選項未被打開時,才能執行事務,不然事務直接返回失敗。
  • Redis 的事務保證了 ACID 中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)。
相關文章
相關標籤/搜索