天天看點

redis事物詳解redis事物詳解

redis事物詳解

Redis 通過 MULTI 、 DISCARD 、 EXEC 和 WATCH 四個指令來實作事務功能, 本章首先讨論使用 MULTI 、 DISCARD 和 EXEC 三個指令實作的一般事務, 然後再來讨論帶有 WATCH 的事務的實作。

因為事務的安全性也非常重要, 是以本章最後通過常見的 ACID 性質對 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"
      

一個事務從開始到執行會經曆以下三個階段:

  1. 開始事務。
  2. 指令入隊。
  3. 執行事務。

下文将分别介紹事務的這三個階段。

開始事務

MULTI 指令的執行标記着事務的開始:

redis> MULTI
OK
      

這個指令唯一做的就是, 将用戶端的 

REDIS_MULTI

 選項打開, 讓用戶端從非事務狀态切換到事務狀态。

redis事物詳解redis事物詳解

指令入隊

當用戶端處于非事務狀态下時, 所有發送給伺服器端的指令都會立即被伺服器執行:

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事物詳解redis事物詳解

事務隊列是一個數組, 每個數組項是都包含三個屬性:

  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
      

那麼程式将為用戶端建立以下事務隊列:

數組索引 cmd argv argc

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 這四個指令 —— 當這四個指令從用戶端發送到伺服器時, 它們會像用戶端處于非事務狀态一樣, 直接被伺服器執行:

redis事物詳解redis事物詳解

如果用戶端正處于事務狀态, 那麼當 EXEC 指令執行時, 伺服器根據用戶端所儲存的事務隊列, 以先進先出(FIFO)的方式執行事務隊列中的指令: 最先入隊的指令最先執行, 而最後入隊的指令最後執行。

比如說,對于以下事務隊列:

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 的順序儲存到一個回複隊列中。

比如說,對于上面給出的事務隊列,程式将為隊列中的指令建立如下回複隊列:

回複類型 回複内容
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 的配置,以及記憶體限制,等等。

不過事務中的指令和普通指令在執行上還是有一點差別的,其中最重要的兩點是:

  1. 非事務狀态下的指令以單個指令為機關執行,前一個指令和後一個指令的用戶端不一定是同一個;

    而事務狀态則是以一個事務為機關,執行事務隊列中的所有指令:除非目前事務執行完畢,否則伺服器不會中斷事務,也不會執行其他用戶端的其他指令。

  2. 在非事務狀态下,執行指令所得的結果會立即被傳回給用戶端;

    而事務則是将所有指令的結果集合到回複隊列,再作為 EXEC 指令的結果傳回給用戶端。

事務狀态下的 DISCARD 、 MULTI 和 WATCH 指令

除了 EXEC 之外, 伺服器在用戶端處于事務狀态時, 不加入到事務隊列而直接執行的另外三個指令是 DISCARD 、 MULTI 和 WATCH 。

DISCARD 指令用于取消一個事務, 它清空用戶端的整個事務隊列, 然後将用戶端從事務狀态調整回非事務狀态, 最後傳回字元串 

OK

 給用戶端, 說明事務已被取消。

Redis 的事務是不可嵌套的, 當用戶端已經處于事務狀态, 而用戶端又再向伺服器發送 MULTI 時, 伺服器隻是簡單地向用戶端發送一個錯誤, 然後繼續等待其他指令的入隊。 MULTI 指令的發送不會造成整個事務失敗, 也不會修改事務隊列中已有的資料。

WATCH 隻能在用戶端進入事務狀态之前執行, 在事務狀态下發送 WATCH 指令會引發一個錯誤, 但它不會造成整個事務失敗, 也不會修改事務隊列中已有的資料(和前面處理 MULTI 的情況一樣)。

帶 WATCH 的事務

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

WATCH 指令的實作

在每個代表資料庫的 

redis.h/redisDb

 結構類型中, 都儲存了一個 

watched_keys

 字典, 字典的鍵是這個資料庫被監視的鍵, 而字典的值則是一個連結清單, 連結清單中儲存了所有監視這個鍵的用戶端。

比如說,以下字典就展示了一個 

watched_keys

 字典的例子:

redis事物詳解redis事物詳解

其中, 鍵 

key1

 正在被 

client2

 、 

client5

 和 

client1

 三個用戶端監視, 其他一些鍵也分别被其他别的用戶端監視着。

WATCH 指令的作用, 就是将目前用戶端和要監視的鍵在 

watched_keys

 中進行關聯。

舉個例子, 如果目前用戶端為 

client10086

 , 那麼當用戶端執行 

WATCH key1 key2

 時, 前面展示的 

watched_keys

 将被修改成這個樣子:

redis事物詳解redis事物詳解

通過 

watched_keys

 字典, 如果程式想檢查某個鍵是否被監視, 那麼它隻要檢查字典中是否存在這個鍵即可; 如果程式要擷取監視某個鍵的所有用戶端, 那麼隻要取出鍵的值(一個連結清單), 然後對連結清單進行周遊即可。

WATCH 的觸發

在任何對資料庫鍵空間(key space)進行修改的指令成功執行之後 (比如 FLUSHDB 、 SET 、 DEL 、 LPUSH 、 SADD 、 ZREM ,諸如此類), 

multi.c/touchWatchedKey

 函數都會被調用 —— 它檢查資料庫的 

watched_keys

 字典, 看是否有用戶端在監視已經被指令修改的鍵, 如果有的話, 程式将所有監視這個/這些被修改鍵的用戶端的 

REDIS_DIRTY_CAS

 選項打開:

redis事物詳解redis事物詳解

當用戶端發送 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

 字典如下圖所示:

redis事物詳解redis事物詳解

如果某個用戶端對 

key1

 進行了修改(比如執行 

DEL key1

 ), 那麼所有監視 

key1

 的用戶端, 包括 

client2

client5

client1

 的 

REDIS_DIRTY_CAS

 選項都會被打開, 當用戶端 

client2

client5

client1

 執行 EXEC 的時候, 它們的事務都會以失敗告終。

最後,當一個用戶端結束它的事務時,無論事務是成功執行,還是失敗, 

watched_keys

 字典中和這個用戶端相關的資料都會被清除。

事務的 ACID 性質

勘誤:Redis 的事務是保證原子性的,本節的内容将原子性和復原功能混淆了,等待修複中。 —— 2013.6.23

在傳統的關系式資料庫中,常常用 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)。

原文位址https://blog.csdn.net/yanshuanche3765/article/details/81271208