Redis 事務(wù)的實現(xiàn)

2018-08-02 14:56 更新

一個事務(wù)從開始到結(jié)束通常會經(jīng)歷以下三個階段:

  1. 事務(wù)開始。
  2. 命令入隊。
  3. 事務(wù)執(zhí)行。

本節(jié)接下來的內(nèi)容將對這三個階段進行介紹, 說明一個事務(wù)從開始到結(jié)束的整個過程。

事務(wù)開始

MULTI 命令的執(zhí)行標(biāo)志著事務(wù)的開始:

redis> MULTI
OK

MULTI 命令可以將執(zhí)行該命令的客戶端從非事務(wù)狀態(tài)切換至事務(wù)狀態(tài), 這一切換是通過在客戶端狀態(tài)的 flags 屬性中打開 REDIS_MULTI 標(biāo)識來完成的, MULTI 命令的實現(xiàn)可以用以下偽代碼來表示:

def MULTI():

    # 打開事務(wù)標(biāo)識
    client.flags |= REDIS_MULTI

    # 返回 OK 回復(fù)
    replyOK()

命令入隊

當(dāng)一個客戶端處于非事務(wù)狀態(tài)時, 這個客戶端發(fā)送的命令會立即被服務(wù)器執(zhí)行:

redis> SET "name" "Practical Common Lisp"
OK

redis> GET "name"
"Practical Common Lisp"

redis> SET "author" "Peter Seibel"
OK

redis> GET "author"
"Peter Seibel"

與此不同的是, 當(dāng)一個客戶端切換到事務(wù)狀態(tài)之后, 服務(wù)器會根據(jù)這個客戶端發(fā)來的不同命令執(zhí)行不同的操作:

  • 如果客戶端發(fā)送的命令為 EXEC 、 DISCARD 、 WATCH 、 MULTI 四個命令的其中一個, 那么服務(wù)器立即執(zhí)行這個命令。
  • 與此相反, 如果客戶端發(fā)送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四個命令以外的其他命令, 那么服務(wù)器并不立即執(zhí)行這個命令, 而是將這個命令放入一個事務(wù)隊列里面, 然后向客戶端返回 QUEUED 回復(fù)。

服務(wù)器判斷命令是該入隊還是該立即執(zhí)行的過程可以用流程圖 IMAGE_ENQUEUE_OR_EXEC 來描述。

事務(wù)隊列

每個 Redis 客戶端都有自己的事務(wù)狀態(tài), 這個事務(wù)狀態(tài)保存在客戶端狀態(tài)的 mstate 屬性里面:

typedef struct redisClient {

    // ...

    // 事務(wù)狀態(tài)
    multiState mstate;      /* MULTI/EXEC state */

    // ...

} redisClient;

事務(wù)狀態(tài)包含一個事務(wù)隊列, 以及一個已入隊命令的計數(shù)器 (也可以說是事務(wù)隊列的長度):

typedef struct multiState {

    // 事務(wù)隊列,F(xiàn)IFO 順序
    multiCmd *commands;

    // 已入隊命令計數(shù)
    int count;

} multiState;

事務(wù)隊列是一個 multiCmd 類型的數(shù)組, 數(shù)組中的每個 multiCmd 結(jié)構(gòu)都保存了一個已入隊命令的相關(guān)信息, 包括指向命令實現(xiàn)函數(shù)的指針, 命令的參數(shù), 以及參數(shù)的數(shù)量:

typedef struct multiCmd {

    // 參數(shù)
    robj **argv;

    // 參數(shù)數(shù)量
    int argc;

    // 命令指針
    struct redisCommand *cmd;

} multiCmd;

事務(wù)隊列以先進先出(FIFO)的方式保存入隊的命令: 較先入隊的命令會被放到數(shù)組的前面, 而較后入隊的命令則會被放到數(shù)組的后面。

舉個例子, 如果客戶端執(zhí)行以下命令:

redis> MULTI
OK

redis> SET "name" "Practical Common Lisp"
QUEUED

redis> GET "name"
QUEUED

redis> SET "author" "Peter Seibel"
QUEUED

redis> GET "author"
QUEUED

那么服務(wù)器將為客戶端創(chuàng)建圖 IMAGE_TRANSACTION_STATE 所示的事務(wù)狀態(tài):

  • 最先入隊的 SET 命令被放在了事務(wù)隊列的索引 0 位置上。
  • 第二入隊的 GET 命令被放在了事務(wù)隊列的索引 1 位置上。
  • 第三入隊的另一個 SET 命令被放在了事務(wù)隊列的索引 2 位置上。
  • 最后入隊的另一個 GET 命令被放在了事務(wù)隊列的索引 3 位置上。

執(zhí)行事務(wù)

當(dāng)一個處于事務(wù)狀態(tài)的客戶端向服務(wù)器發(fā)送 EXEC 命令時, 這個 EXEC 命令將立即被服務(wù)器執(zhí)行: 服務(wù)器會遍歷這個客戶端的事務(wù)隊列, 執(zhí)行隊列中保存的所有命令, 最后將執(zhí)行命令所得的結(jié)果全部返回給客戶端。

舉個例子, 對于圖 IMAGE_TRANSACTION_STATE 所示的事務(wù)隊列來說, 服務(wù)器首先會執(zhí)行命令:

SET "name" "Practical Common Lisp"

接著執(zhí)行命令:

GET "name"

之后執(zhí)行命令:

SET "author" "Peter Seibel"

再之后執(zhí)行命令:

GET "author"

最后, 服務(wù)器會將執(zhí)行這四個命令所得的回復(fù)返回給客戶端:

redis> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"

EXEC 命令的實現(xiàn)原理可以用以下偽代碼來描述:

def EXEC():

    # 創(chuàng)建空白的回復(fù)隊列
    reply_queue = []

    # 遍歷事務(wù)隊列中的每個項
    # 讀取命令的參數(shù),參數(shù)的個數(shù),以及要執(zhí)行的命令
    for argv, argc, cmd in client.mstate.commands:

        # 執(zhí)行命令,并取得命令的返回值
        reply = execute_command(cmd, argv, argc)

        # 將返回值追加到回復(fù)隊列末尾
        reply_queue.append(reply)

    # 移除 REDIS_MULTI 標(biāo)識,讓客戶端回到非事務(wù)狀態(tài)
    client.flags &= ~REDIS_MULTI

    # 清空客戶端的事務(wù)狀態(tài),包括:
    # 1)清零入隊命令計數(shù)器
    # 2)釋放事務(wù)隊列
    client.mstate.count = 0
    release_transaction_queue(client.mstate.commands)

    # 將事務(wù)的執(zhí)行結(jié)果返回給客戶端
    send_reply_to_client(client, reply_queue)
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號