Redis事務源碼解析

事務定義:將多個命令打包,然後一次性、按順序執行多個命令。在執行命令期間(EXEC),不會中斷事務而去執行其他客戶端的命令請求。滿足ACID中的原子性、一致性和隔離性。

舉個例子:

redis 127.0.0.1:6379> MULTI                --------------事務開始命令
OK
redis 127.0.0.1:6379> SET name wqh         --------------命令1入隊
QUEUED                                     --------------命令1回覆
redis 127.0.0.1:6379> GET name             --------------命令2入隊
QUEUED                                     --------------命令2回覆
redis 127.0.0.1:6379> EXEC                 --------------事務執行命令
1) OK                                      --------------事務統一回復
2) "wqh"

MULTI命令標誌着事務的開始,該命令將客戶端從非事務狀態切換到事務狀態,在客戶端狀態的flags屬性中,打開標識REDIS_MULTI(1<<3)。源碼如下:

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(立刻執行)這四個命令以外的其他命令,服務器將不會立即執行該命令,而且放入事務隊列中,向客戶端返回QUEUED恢復。

Redis客戶端的事務狀態是保存在mstate狀態中:

typedef struct redisClient {
    // ...
    // 事務狀態
    multiState mstate;      /* MULTI/EXEC state */
    // ...
}

事務狀態定義爲:

/*
 * 事務狀態
 */
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;

可以看出,事無狀態主要由一個事務隊列和一個已入隊命令的計數器組成。事務隊列以先進先出(FIFO)方式進行,是一個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++;
}

執行事務:執行EXEC命令,服務器遍歷這個客戶端的事務隊列,執行所有命令,最後將執行結果全部返回給客戶端。該命令的執行源碼爲:

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);
}

Watch命令實現

Watch命令,樂觀鎖,可以實現對任意數量的數據庫鍵的監視,在EXEC命令執行時,會檢查被監視的鍵是否至少有一個已經被修改過了,如果是則拒絕執行事務,並返回空回覆nil。

watch命令的執行:不能在事務開始後執行

void watchCommand(redisClient *c) {
    int j;

    // 不能在事務開始後執行
    if (c->flags & REDIS_MULTI) {
        addReplyError(c,"WATCH inside MULTI is not allowed");
        return;
    }

    // 監視輸入的任意個鍵
    for (j = 1; j < c->argc; j++)
    addReply(c,shared.ok);
}

void unwatchCommand(redisClient *c) {

    // 取消客戶端對所有鍵的監視
    unwatchAllKeys(c);
    
    // 重置狀態
    c->flags &= (~REDIS_DIRTY_CAS);

    addReply(c,shared.ok);
}
監視一個鍵的定義爲:
typedef struct watchedKey {

    // 被監視的鍵
    robj *key;

    // 鍵所在的數據庫
    redisDb *db;

} watchedKey;

可以看出,包含被監視的鍵和該鍵所在的數據庫。

每個數據庫redisDb都保存着一個watched_keys字典,字典的鍵是被WATCH命令所監視的某個鍵,值是一個鏈表,該鏈表記錄着所有監視該鍵的客戶端。

typedef struct redisDb {
    // 正在被 WATCH 命令監視的鍵
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
}

客戶端狀態下也存放着被該客戶端監視的鍵的鏈表集合。

typedef struct redisClient {
    // 被監視的鍵
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
}

因此有些操作需要同時對上述兩個屬性進行操作:

(1)客戶端監視指定鍵:

/* Watch for the specified key 
 *
 * 讓客戶端 c 監視給定的鍵 key
 */
void watchForKey(redisClient *c, robj *key) {

    list *clients = NULL;
    listIter li;
    listNode *ln;
    watchedKey *wk;

    /* Check if we are already watching for this key */
    // 檢查 key 是否已經保存在 watched_keys 鏈表中,
    // 如果是的話,直接返回
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        wk = listNodeValue(ln);
        if (wk->db == c->db && equalStringObjects(key,wk->key))
            return; /* Key already watched */
    }

    // 鍵不存在於 watched_keys ,添加它

    // 以下是一個 key 不存在於字典的例子:
    // before :
    // {
    //  'key-1' : [c1, c2, c3],
    //  'key-2' : [c1, c2],
    // }
    // after c-10086 WATCH key-1 and key-3:
    // {
    //  'key-1' : [c1, c2, c3, c-10086],
    //  'key-2' : [c1, c2],
    //  'key-3' : [c-10086]
    // }

    /* This key is not already watched in this DB. Let's add it */
    // 檢查 key 是否存在於數據庫的 watched_keys 字典中
    clients = dictFetchValue(c->db->watched_keys,key);
    // 如果不存在的話,添加它
    if (!clients) { 
        // 值爲鏈表
        clients = listCreate();
        // 關聯鍵值對到字典
        dictAdd(c->db->watched_keys,key,clients);
        incrRefCount(key);
    }
    // 將客戶端添加到鏈表的末尾
    listAddNodeTail(clients,c);

    /* Add the new key to the list of keys watched by this client */
    // 將新 watchedKey 結構添加到客戶端 watched_keys 鏈表的表尾
    // 以下是一個添加 watchedKey 結構的例子
    // before:
    // [
    //  {
    //   'key': 'key-1',
    //   'db' : 0
    //  }
    // ]
    // after client watch key-123321 in db 0:
    // [
    //  {
    //   'key': 'key-1',
    //   'db' : 0
    //  }
    //  ,
    //  {
    //   'key': 'key-123321',
    //   'db': 0
    //  }
    // ]
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);
}

(2)取消客戶端對所有鍵的監視:

/* Unwatch all the keys watched by this client. To clean the EXEC dirty
 * flag is up to the caller. 
 *
 * 取消客戶端對所有鍵的監視。
 *
 * 清除客戶端事務狀態的任務由調用者執行。
 */
void unwatchAllKeys(redisClient *c) {
    listIter li;
    listNode *ln;

    // 沒有鍵被監視,直接返回
    if (listLength(c->watched_keys) == 0) return;

    // 遍歷鏈表中所有被客戶端監視的鍵
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        list *clients;
        watchedKey *wk;

        /* Lookup the watched key -> clients list and remove the client
         * from the list */
        // 從數據庫的 watched_keys 字典的 key 鍵中
        // 刪除鏈表裏包含的客戶端節點
        wk = listNodeValue(ln);
        // 取出客戶端鏈表
        clients = dictFetchValue(wk->db->watched_keys, wk->key);
        redisAssertWithInfo(c,NULL,clients != NULL);
        // 刪除鏈表中的客戶端節點
        listDelNode(clients,listSearchKey(clients,c));

        /* Kill the entry at all if this was the only client */
        // 如果鏈表已經被清空,那麼刪除這個鍵
        if (listLength(clients) == 0)
            dictDelete(wk->db->watched_keys, wk->key);

        /* Remove this watched key from the client->watched list */
        // 從鏈表中移除 key 節點
        listDelNode(c->watched_keys,ln);

        decrRefCount(wk->key);
        zfree(wk);
    }
}

監視機制的觸發

所有對客戶端執行修改的命令,如SET,LPUSH,FLUSHDB等,執行之後都會調用相關函數對watched_keys字典進行檢查,查看被該客戶端監視的鍵是否被上述命令修改過,如果是,則監視被修改鍵的客戶端的REDIS_DIRTY_CAS標識(1<<5)打開,表明該客戶端的事務安全性已經被破壞。

這裏面,標識被打開的函數有兩個:

(1)touchWatchedkey(db, key)

/* "Touch" a key, so that if this key is being WATCHed by some client the
 * next EXEC will fail. 
 *
 * “觸碰”一個鍵,如果這個鍵正在被某個/某些客戶端監視着,
 * 那麼這個/這些客戶端在執行 EXEC 時事務將失敗。
 */
void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;

    // 字典爲空,沒有任何鍵被監視
    if (dictSize(db->watched_keys) == 0) return;

    // 獲取所有監視這個鍵的客戶端
    clients = dictFetchValue(db->watched_keys, key);
    if (!clients) return;

    /* Mark all the clients watching this key as REDIS_DIRTY_CAS */
    /* Check if we are already watching for this key */
    // 遍歷所有客戶端,打開他們的 REDIS_DIRTY_CAS 標識
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
        redisClient *c = listNodeValue(ln);

        c->flags |= REDIS_DIRTY_CAS;
    }
}

(2)touchWatchedKeysOnFlush(dbid):FLUSHDB/FLUSHALL命令

/* On FLUSHDB or FLUSHALL all the watched keys that are present before the
 * flush but will be deleted as effect of the flushing operation should
 * be touched. "dbid" is the DB that's getting the flush. -1 if it is
 * a FLUSHALL operation (all the DBs flushed). 
 *
 * 當一個數據庫被 FLUSHDB 或者 FLUSHALL 清空時,
 * 它數據庫內的所有 key 都應該被觸碰。
 *
 * dbid 參數指定要被 FLUSH 的數據庫。
 *
 * 如果 dbid 爲 -1 ,那麼表示執行的是 FLUSHALL ,
 * 所有數據庫都將被 FLUSH
 */
void touchWatchedKeysOnFlush(int dbid) {
    listIter li1, li2;
    listNode *ln;

    // 這裏的思路挺有趣的,不是遍歷數據庫的所有 key 來讓客戶端變爲 DIRTY
    // 而是遍歷所有客戶端,然後遍歷客戶端監視的鍵,再讓相應的客戶端變爲 DIRTY
    // 後者要比前者高效很多

    /* For every client, check all the waited keys */
    // 遍歷所有客戶端
    listRewind(server.clients,&li1);
    while((ln = listNext(&li1))) {

        redisClient *c = listNodeValue(ln);

        // 遍歷客戶端監視的鍵
        listRewind(c->watched_keys,&li2);
        while((ln = listNext(&li2))) {

            // 取出監視的鍵和鍵的數據庫
            watchedKey *wk = listNodeValue(ln);

            /* For every watched key matching the specified DB, if the
             * key exists, mark the client as dirty, as the key will be
             * removed. */
            // 如果數據庫號碼相同,或者執行的命令爲 FLUSHALL
            // 那麼將客戶端設置爲 REDIS_DIRTY_CAS
            if (dbid == -1 || wk->db->id == dbid) {
                if (dictFind(wk->db->dict, wk->key->ptr) != NULL)
                    c->flags |= REDIS_DIRTY_CAS;
            }
        }
    }
}
當服務接收到一個客戶端發來的EXEC命令後,首先會檢測這個客戶端是否打開了REDIS_DIRTY_CAS標識來決定是否執行事務。如果打開了說明至少有一個鍵被修改過了,事務不再安全,會拒絕執行事務,反之則執行事務。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章