高頻Redis面試題解析:Redis 事務是否具有原子性?

1、Redis 事務的實現原理

一個事務從開始到結束一般會經歷如下三個階段:程序員

一、事務開始面試

客戶端發送 MULTI 命令,服務器執行 MULTI 命令邏輯。redis

服務器會在客戶端狀態(redisClient)的 flags 屬性打開 REDIS_MULTI 標識,將客戶端從非事務狀態切換到事務狀態。數據庫

void multiCommand(redisClient *c) {
    // 不能在事務中嵌套事務
    if (c->flags & REDIS_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");
        return;
    }
    // 打開事務 FLAG
    c->flags |= REDIS_MULTI;
    addReply(c,shared.ok);
}

二、命令入隊編程

接着,用戶能夠在客戶端輸入當前事務要執行的多個命令。數組

當客戶端切換到事務狀態時,服務器會根據客戶端發來的命令來執行不一樣的操做。安全

  • 若是客戶端發送的命令爲 EXEC、DISCARD、WATCH、MULTI 四個命令的其中一個,那麼服務器當即執行這個命令。
  • 與此相反,若是客戶端發送的命令是 EXEC、DISCARD、WATCH、MULTI 四個命令之外的其餘命令,那麼服務器並不當即執行這個命令。
    • 首先檢查此命令的格式是否正確,若是不正確,服務器會在客戶端狀態(redisClient)的 flags 屬性打開 REDIS_MULTI 標識,而且返回錯誤信息給客戶端。
  • 若是正確將這個命令放入一個事務隊列裏面,而後向客戶端返回 QUEUED 回覆。

咱們先看看事務隊列是如何實現的服務器

每一個 Redis 客戶端都有本身的事務狀態,對應的是客戶端狀態(redisClient)的 mstate 屬性。ide

typeof struct redisClient{
    // 事務狀態
    multiState mstate;
}
redisClient;

事務狀態(mstate)包含一個事務隊列(FIFO 隊列),以及一個已入隊命令的計數器。函數

/*
 * 事務狀態
 */
typedef struct multiState {
    // 事務隊列,FIFO 順序
    multiCmd *commands;
    /* Array of MULTI commands */
    // 已入隊命令計數
    int count;
    /* Total number of MULTI commands */
    int minreplicas;
    /* MINREPLICAS for synchronous replication */
    time_t minreplicas_timeout;
    /* MINREPLICAS timeout as unixtime. */
}
multiState;

事務隊列是一個 multiCmd 類型數組,數組中每一個 multiCmd 結構都保存了一個如入隊命令的相關信息:指向命令實現函數的指針,命令的參數,以及參數的數量。

/*
 * 事務命令
 */
typedef struct multiCmd {
    // 參數
    robj **argv;
    // 參數數量
    int argc;
    // 命令指針
    struct redisCommand *cmd;
}
multiCmd;

最後咱們再看看入隊列的源碼

/* Add a new command into the MULTI commands queue 
 *
 * 將一個新命令添加到事務隊列中
 */
void queueMultiCommand(redisClient *c) {
    multiCmd *mc;
    int j;
    // 爲新數組元素分配空間
    c->mstate.commands = zrealloc(c->mstate.commands,
                sizeof(multiCmd)*(c->mstate.count+1));
    // 指向新元素
    mc = c->mstate.commands+c->mstate.count;
    // 設置事務的命令、命令參數數量,以及命令的參數
    mc->cmd = c->cmd;
    mc->argc = c->argc;
    mc->argv = zmalloc(sizeof(robj*)*c->argc);
    memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);
    for (j = 0; j < c->argc; j++)
            incrRefCount(mc->argv[j]);
    // 事務命令數量計數器增一
    c->mstate.count++;
}

固然了,還有咱們上面提到的,若是命令入隊出錯時,會打開客戶端狀態的 REDIS_DIRTY_EXEC 標識。

/* Flag the transacation as DIRTY_EXEC so that EXEC will fail.
 *
 * 將事務狀態設爲 DIRTY_EXEC ,讓以後的 EXEC 命令失敗。
 *
 * Should be called every time there is an error while queueing a command. 
 *
 * 每次在入隊命令出錯時調用
 */
void flagTransaction(redisClient *c) {
    if (c->flags & REDIS_MULTI)
            c->flags |= REDIS_DIRTY_EXEC;
}

三、事務執行

客戶端發送 EXEC 命令,服務器執行 EXEC 命令邏輯。

  • 若是客戶端狀態的 flags 屬性不包含 REDIS_MULTI 標識,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 標識,那麼就直接取消事務的執行。
  • 不然客戶端處於事務狀態(flags 有 REDIS_MULTI 標識),服務器會遍歷客戶端的事務隊列,而後執行事務隊列中的全部命令,最後將返回結果所有返回給客戶端;
    void execCommand(redisClient *c) {
    int j;
    robj **orig_argv;
    int orig_argc;
    struct redisCommand *orig_cmd;
    int must_propagate = 0;
    /* Need to propagate MULTI/EXEC to AOF / slaves? */
    // 客戶端沒有執行事務
    if (!(c->flags & REDIS_MULTI)) {
        addReplyError(c,"EXEC without MULTI");
        return;
    }
    /* Check if we need to abort the EXEC because:
     *
     * 檢查是否須要阻止事務執行,由於:
     *
     * 1) Some WATCHed key was touched.
     *    有被監視的鍵已經被修改了
     *
     * 2) There was a previous error while queueing commands.
     *    命令在入隊時發生錯誤
     *    (注意這個行爲是 2.6.4 之後才修改的,以前是靜默處理入隊出錯命令)
     *
     * A failed EXEC in the first case returns a multi bulk nil object
     * (technically it is not an error but a special behavior), while
     * in the second an EXECABORT error is returned. 
     *
     * 第一種狀況返回多個批量回復的空對象
     * 而第二種狀況則返回一個 EXECABORT 錯誤
     */
    if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
        addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
                                                          shared.nullmultibulk);
        // 取消事務
        discardTransaction(c);
        goto handle_monitor;
    }
    /* Exec all the queued commands */
    // 已經能夠保證安全性了,取消客戶端對全部鍵的監視
    unwatchAllKeys(c);
    /* Unwatch ASAP otherwise we'll waste CPU cycles */
    // 由於事務中的命令在執行時可能會修改命令和命令的參數
    // 因此爲了正確地傳播命令,須要現備份這些命令和參數
    orig_argv = c->argv;
    orig_argc = c->argc;
    orig_cmd = c->cmd;
    addReplyMultiBulkLen(c,c->mstate.count);
    // 執行事務中的命令
    for (j = 0; j < c->mstate.count; j++) {
        // 由於 Redis 的命令必須在客戶端的上下文中執行
        // 因此要將事務隊列中的命令、命令參數等設置給客戶端
        c->argc = c->mstate.commands[j].argc;
        c->argv = c->mstate.commands[j].argv;
        c->cmd = c->mstate.commands[j].cmd;
        /* Propagate a MULTI request once we encounter the first write op.
         *
         * 當趕上第一個寫命令時,傳播 MULTI 命令。
         *
         * This way we'll deliver the MULTI/..../EXEC block as a whole and
         * both the AOF and the replication link will have the same consistency
         * and atomicity guarantees. 
         *
         * 這能夠確保服務器和 AOF 文件以及附屬節點的數據一致性。
         */
        if (!must_propagate && !(c->cmd->flags & REDIS_CMD_READONLY)) {
            // 傳播 MULTI 命令
            execCommandPropagateMulti(c);
            // 計數器,只發送一次
            must_propagate = 1;
        }
        // 執行命令
        call(c,REDIS_CALL_FULL);
        /* Commands may alter argc/argv, restore mstate. */
        // 由於執行後命令、命令參數可能會被改變
        // 好比 SPOP 會被改寫爲 SREM
        // 因此這裏須要更新事務隊列中的命令和參數
        // 確保附屬節點和 AOF 的數據一致性
        c->mstate.commands[j].argc = c->argc;
        c->mstate.commands[j].argv = c->argv;
        c->mstate.commands[j].cmd = c->cmd;
    }
    // 還原命令、命令參數
    c->argv = orig_argv;
    c->argc = orig_argc;
    c->cmd = orig_cmd;
    // 清理事務狀態
    discardTransaction(c);
    /* Make sure the EXEC command will be propagated as well if MULTI
     * was already propagated. */
    // 將服務器設爲髒,確保 EXEC 命令也會被傳播
    if (must_propagate) server.dirty++;
    handle_monitor:
    /* Send EXEC to clients waiting data from MONITOR. We do it here
     * since the natural order of commands execution is actually:
     * MUTLI, EXEC, ... commands inside transaction ...
     * Instead EXEC is flagged as REDIS_CMD_SKIP_MONITOR in the command
     * table, and we do it here with correct ordering. */
    if (listLength(server.monitors) && !server.loading)
            replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
    }

2、爲何不少人說 Redis 事務爲什麼不支持原子性?

一、Redis 事務不支持事務回滾機制

Redis 事務執行過程當中,若是一個命令執行出錯,那麼就返回錯誤,而後仍是會接着繼續執行下面的命令。

下面咱們演示一下:

高頻Redis面試題解析:Redis 事務是否具有原子性?

正是由於 Redis 事務不支持事務回滾機制,若是事務執行中出現了命令執行錯誤(例如對 String 類型的數據庫鍵執行 LPUSH 操做),只會返回當前命令執行的錯誤給客戶端,並不會影響下面的命令的執行。因此不少人以爲和關係型數據庫(MySQL) 不同,而 MySQL 的事務是具備原子性的,因此你們都認爲 Redis 事務不支持原子性。

二、可是其實 Redis 意義上是支持原子性的

正常狀況下,它也是要不全部命令執行成功,要不一個命令都不執行。

咱們下面演示一下:

所有執行成功的:

高頻Redis面試題解析:Redis 事務是否具有原子性?

一個都不執行:

高頻Redis面試題解析:Redis 事務是否具有原子性?

這就是上面提到的,在事務開始後,用戶能夠輸入事務要執行的命令;在命令入事務隊列前,會對命令進行檢查,若是命令不存在或者是命令參數不對,則會返回錯誤可客戶端,而且修改客戶端狀態。

當後面客戶端執行 EXEC 命令時,服務器就會直接拒絕執行此事務了。

因此說,Redis 事務實際上是支持原子性的!即便 Redis 不支持事務回滾機制,可是它會檢查每個事務中的命令是否錯誤。

可是咱們要注意一個點就是:Redis 事務不支持檢查那些程序員本身邏輯錯誤。例如對 String 類型的數據庫鍵執行對 HashMap 類型的操做!

我很贊同 Redis 做者的想法

首先,MySQL 和 Redis 的定位不同,一個是關係型數據庫,一個是 NoSQL。

MySQL 的 SQL 查詢是能夠至關複雜的,並且 MySQL 沒有事務隊列這種說法,SQL 真正開始執行纔會進行分析和檢查,MySQL 不可能提早知道下一條 SQL 是否正確。因此支持事務回滾是很是有必要的~

可是,Redis 使用了事務隊列來預先將執行命令存儲起來,而且會對其進行格式檢查的,提早就知道命令是否可執行了。因此若是隻要有一個命令是錯誤的,那麼這個事務是不能執行的。

Redis 做者認爲基本只會出如今開發環境的編程錯誤其實在生產環境基本是不可能出現的(例如對 String 類型的數據庫鍵執行 LPUSH 操做),因此他以爲不必爲了這事務回滾機制而改變 Redis 追求簡單高效的設計主旨

相關文章
相關標籤/搜索