Redis源碼解析(22) 事務


其他文章:
Redis源碼解析(1) 動態字符串與鏈表
Redis源碼解析(2) 字典與迭代器
Redis源碼解析(3) 跳躍表
Redis源碼解析(4) 整數集合
Redis源碼解析(5) 壓縮列表
Redis源碼解析(6) 鍵的過期處理策略
Redis源碼解析(7) 發佈訂閱機制
Redis源碼解析(8) AOF持久化
Redis源碼解析(9) RDB持久化
Redis源碼解析(10) 網絡框架
Redis源碼解析(11) 內存淘汰策略
Redis源碼解析(12) 命令執行過程
Redis源碼解析(13) 主從複製
Redis源碼解析(14) 哨兵機制[1] 結構與初始化
Redis源碼解析(15) 哨兵機制[2] 信息同步與TILT模式
Redis源碼解析(16) 哨兵機制[3] 判斷下線
Redis源碼解析(17) 哨兵機制[4] 故障轉移
Redis源碼解析(18) 集羣[1]初始化,握手與心跳檢測
Redis源碼解析(19) 集羣[2] 主從複製,故障檢測與故障轉移
Redis源碼解析(20) 集羣[3] 鍵的存儲,重新分片與重定向
Redis源碼解析(21) 集羣[4] 故障轉移failover與slave遷移
Redis源碼解析(22) 事務
Redis源碼解析(23) SCAN命令實現

引言

redis通過MULTl,EXEC,WATCH,DISCARD來實現事務.

因爲redis本身是一個單線程的服務器,所有的請求會在IO多路複用處變爲串行請求,這其實意味着事務的隔離性是天然的,相比於關係型數據庫中複雜的隔離性,redis中就顯得尤爲簡潔.然後持久性又是由配置決定的.所以重點討論的其實就是原子性和一致性.

那麼原子性是如何實現的呢,其實redis中的一個事務處理過程是這樣的:

  1. 客戶端發送MULTl,開始一次事務,所做的事情其實是打開REDIS_MULTI標識
  2. 客戶端發送此次事務中的命令,服務器把所有的命令組成一個隊列,暫不執行
  3. 客戶端執行EXEC,服務器執行所有的命令.一次事務完成

用這種打包命令的方式,我們可以很容易的實現事務的原子性,這種原子性是要麼都執行要麼都不執行,而不是要麼都執行成功,要麼都不執行,因爲redis不支持回滾,也就是單條數據支持原子性,事務不支持原子性.那麼一致性呢?一致性就是在執行事務之前數據庫是一致的,在事務完成以後,數據庫也是一致的,這裏的一致用關係型數據庫的的名詞來說就是滿足一致性約束,也就是數據滿足數據庫要求,這裏其實對輸入的命令進行檢查就可以了.但是redis事務真的能做到數據一致性嗎?其實非也,不管是是事務也好,普通語句也好其實在整個redis的分佈式系統中都只是滿足最終一致性罷了,畢竟redis的設計初衷就是滿足AP而已.這樣事務的ACID特性中隔離性就達到了,而持久性取決於設置.

但是有一點值得一談,就是我們都知道關係型數據庫使用redo日誌來進行事務回滾,而redis是不支持事務回滾的,這也意味着在一次事務執行中如果某個語句失敗我們是不會進行回滾的,那麼爲什麼redis不支持回滾呢?

只有當被調用的Redis命令有語法錯誤時,這條命令纔會執行失敗(在將這個命令放入事務隊列期間,Redis能夠發現此類問題),或者對某個鍵執行不符合其數據類型的操作:實際上,這就意味着只有程序錯誤纔會導致Redis命令執行失敗,這種錯誤很有可能在程序開發期間發現,一般很少在生產環境發現。
Redis已經在系統內部進行功能簡化,這樣可以確保更快的運行速度,因爲Redis不需要事務回滾的能力。對於Redis事務的這種行爲,有一個普遍的反對觀點,那就是程序有可能會有缺陷(bug)。但是,你應當注意到:事務回滾並不能解決任何程序錯誤。例如,如果某個查詢會將一個鍵的值遞增2,而不是1,或者遞增錯誤的鍵,那麼事務回滾機制是沒有辦法解決這些程序問題的。

基礎數據結構

typedef struct redisClient {
    ..........
    multiState mstate;      /* MULTI/EXEC state */ // 用於記錄每個客戶端的事務信息
    ..........
} redisClient;
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;

typedef struct multiCmd { // 相當於一個命令的全部參數

    // 參數
    robj **argv;

    // 參數數量
    int argc;

    // 命令指針
    struct redisCommand *cmd;

} multiCmd;

MULTl

我們來看看MULTl的實現,

void multiCommand(redisClient *c) {

    // 不能在事務中嵌套事務
    if (c->flags & REDIS_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");
        return;
    }

    // 打開事務 FLAG
    c->flags |= REDIS_MULTI;
	// 返回OK
    addReply(c,shared.ok);
}

REDIS_MULTI設置在客戶端時,後面這個客戶端發送的信息就會在進行合法性判斷以後加入鏈表,處理邏輯在processCommand中.

    /* Exec the command */
    if (c->flags & REDIS_MULTI && // 標記爲REDIS_MULTI
    	// 命令不爲MULTl,EXEC,WATCH,DISCARD
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
    {
        // 除了上面四個命令以外所有命令都會被入隊到事務隊列中
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else {
		..........
    }
 
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)) { // 鍵在watch時被修改
        // 判斷到底是命令入隊時錯誤還是被監視的鍵被修改
        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)) {

            // 向從節點和AOF文件追加一個MULTI命令
            execCommandPropagateMulti(c);

            // 計數器,只發送一次
            must_propagate = 1;
        }

        // 執行命令 會轉移到從節點和AOF文件中
        call(c,REDIS_CALL_FULL);

        /* Commands may alter argc/argv, restore mstate. */
        // 因爲執行後命令、命令參數可能會被改變
        // 比如 SPOP 會被改寫爲 SREM
        // 其中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);
}

這裏可能會出現兩種錯誤:

  1. 命令格式錯誤
  2. 命令格式正確,但執行參數錯誤,

舉兩個簡單的例子

  1. redis > hello world hello world不是命令 這在processCommand就會檢測出來
  2. redis > ZADD msg hello 其中msg並不是一個有序set對象,這隻能在執行函數的時候檢測出來,也就是call中,但是redis只會回覆錯誤,後面的命令還是會繼續執行,並返回其應該返回的結果.

WATCH

WATCH其實就是一個樂觀鎖,它會在EXEC發送之前監視所有的鍵,當鍵被修改的時候就會修改監視這個值的客戶端的flag,這樣在EXEC執行的時候就可以發現哪些值被修改了.基礎數據結構如下


typedef struct redisDb {
    ........
    // 鍵爲監視的鍵的名稱 值爲客戶端鏈表 代表了現在正在監視這個鍵的客戶端
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    ........
} redisDb;

執行邏輯在watchCommand

// 命令格式爲 WATCH  <key1> [<key2> <key3> ...]
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++)
        watchForKey(c,c->argv[j]);

    addReply(c,shared.ok);
}
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 鏈表中,
    // 如果是的話,直接返回
    // 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 鏈表的表尾
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);
}

這樣這個鍵現在就是被監控的了,那麼監控的作用是什麼呢,就是在監控期間如果這個鍵被修改,那麼在監視這個鍵的客戶端中設置flag爲REDIS_DIRTY_CAS.具體了處理流程是這樣的signalModifiedKey->touchWatchedKey

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

如果一個被監視的鍵在事務期間被修改,這次事務就是失敗的.

參考:
http://www.webyang.net/Html/web/article_411.html

https://blog.csdn.net/gqtcgq/article/details/51842199

https://blog.csdn.net/zdyueguanyun/article/details/73477883?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522158582418519725222439990%2522%252C%2522scm%2522%253A%252220140713.130056874…%2522%257D&request_id=158582418519725222439990&biz_id=0&utm_source=distribute.pc_search_result.none-task

分佈式事務的一致性

分佈式事務方案 - 最終一致性

分佈式事務八_可靠消息最終一致性方案

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