redis原始碼分析之事務Transaction(上)

凌風郎少發表於2017-11-14

這周學習了一下redis事務功能的實現原理,本來是想用一篇文章進行總結的,寫完以後發現這塊內容比較多,而且多個命令之間又互相依賴,放在一篇文章裡一方面篇幅會比較大,另一方面文章組織結構會比較亂,不容易閱讀。因此把事務這個模組整理成上下兩篇文章進行總結。

原文地址:www.jianshu.com/p/acb97d620…

這篇文章我們重點分析一下redis事務命令中的兩個輔助命令:watch跟unwatch。

一、redis事務輔助命令簡介

依然從server.c檔案的命令表中找到相應的命令以及它們對應的處理函式。

//watch,unwatch兩個命令我們把它們叫做redis事務輔助命令
{"watch",watchCommand,-2,"sF",0,NULL,1,-1,1,0,0},
{"unwatch",unwatchCommand,1,"sF",0,NULL,0,0,0,0,0},複製程式碼
  1. watch,用於客戶端關注某個key,當這個key的值被修改時,整個事務就會執行失敗(注:該命令需要在事務開啟前使用)。
  2. unwatch,用於客戶端取消已經watch的key。

用法舉例如下:
clientA

127.0.0.1:6379> watch a
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set b b
QUEUED
//在執行前插入clientB的操作如下,事務就會執行失敗
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>複製程式碼

clientB

127.0.0.1:6379> set a aa
OK
127.0.0.1:6379>複製程式碼

二、redis事務輔助命令原始碼分析

在看具體執行函式之前首先了解幾個資料結構:

//每個客戶端物件中有一個watched_keys連結串列來儲存已經watch的key
typedef struct client {
    list *watched_keys;  
}
//上述連結串列中每個節點的資料結構
typedef struct watchedKey {
    //watch的key
    robj *key;
    //指向的DB,後面細說
    redisDb *db;
} watchedKey;複製程式碼

關於事務的幾個命令所對應的函式都放在了multi.c檔案中。
一起看下watch命令對應處理函式的原始碼:

void watchCommand(client *c) {
    int j;
    //如果客戶端處於事務狀態,則返回錯誤資訊
    //由此可以看出,watch必須在事務開啟前使用
    if (c->flags & CLIENT_MULTI) {
        addReplyError(c,"WATCH inside MULTI is not allowed");
        return;
    }
    //依次watch客戶端的各個引數(這裡說明watch命令可以一次watch多個key)
    //注:0表示命令本身,所以引數從1開始
    for (j = 1; j < c->argc; j++)
        watchForKey(c,c->argv[j]);
    //返回結果
    addReply(c,shared.ok);
}

//具體的watch操作,程式碼較長,慢慢分析
void watchForKey(client *c, robj *key) {
    list *clients = NULL;
    listIter li;
    listNode *ln;
    //上面已經提到了資料結構
    watchedKey *wk;

    //首先判斷key是否已經被客戶端watch
    //listRewind這個函式在釋出訂閱那篇文章裡也有,就是把客戶端的watched_keys賦值給li
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        wk = listNodeValue(ln);
        //這裡一個wk節點中有db,key兩個欄位
        if (wk->db == c->db && equalStringObjects(key,wk->key))
            return; 
    }
    //開始watch指定key
    //整個watch操作儲存了兩套資料結構,一套是在db->watched_keys中的字典結構,如下:
    clients = dictFetchValue(c->db->watched_keys,key);
    //如果是key第一次出現,則進行初始化
    if (!clients) {
        clients = listCreate();
        dictAdd(c->db->watched_keys,key,clients);
        incrRefCount(key);
    }
    //把當前客戶端加到該key的watch連結串列中
    listAddNodeTail(clients,c);
    //另一套是在c->watched_keys中的連結串列結構:如下
    wk = zmalloc(sizeof(*wk));
    //初始化各個欄位
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    //加入到連結串列最後
    listAddNodeTail(c->watched_keys,wk);
}複製程式碼

整個watch的資料結構比較複雜,我這裡畫了一張圖方便理解:

watch資料結構
watch資料結構

簡單解釋一下上面的圖,首先redis把每個客戶端連線包裝成了一個client物件,上圖中db,watch_keys就是其中的兩個欄位(client物件裡面還有很多其他欄位,包括上篇文章中提到的pub/sub)。

  1. db欄位指向給該client物件分配的儲存空間,db物件中也含有一個watched_keys欄位,是字典型別(也就是雜湊表),以想要watch的key做key,儲存的連結串列則是所有watch該key的客戶端。
  2. watch_keys欄位則是一個連結串列型別,每個節點型別為watch_key,其中包含兩個欄位,key表示watch的key,db則指向了當前client物件的db欄位,如上圖。

看完watch命令的原始碼以後,再來看一下unwatch命令,如果搞明白了上面提到的兩套資料結構,那麼看unwatch的原始碼應該會比較容易,畢竟就是刪除資料結構中對應的內容。

void unwatchCommand(client *c) {
    //取消watch所有key
    unwatchAllKeys(c);
    //修改客戶端狀態
    c->flags &= (~CLIENT_DIRTY_CAS);
    addReply(c,shared.ok);
}

//取消watch的key
void unwatchAllKeys(client *c) {
    listIter li;
    listNode *ln;
    //如果客戶端沒有watch任何key,則直接返回
    if (listLength(c->watched_keys) == 0) return;
    //注意這裡操作的是連結串列欄位
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        list *clients;
        watchedKey *wk;
        //遍歷取出該客戶端watch的key
        wk = listNodeValue(ln);
        //取出所有watch了該key的客戶端,這裡則是字典(即雜湊表)
        clients = dictFetchValue(wk->db->watched_keys, wk->key);
        //空指標判斷
        serverAssertWithInfo(c,NULL,clients != NULL);
        //從watch列表中刪除該客戶端
        listDelNode(clients,listSearchKey(clients,c));
        //如果key只有一個當前客戶端watch,則刪除
        if (listLength(clients) == 0)
            dictDelete(wk->db->watched_keys, wk->key);
        //從當前client的watch列表中刪除該key
        listDelNode(c->watched_keys,ln);
        //減少引用數
        decrRefCount(wk->key);
        //釋放記憶體
        zfree(wk);
    }
}複製程式碼

最後我們考慮一下watch機制的觸發時機,現在我們已經把想要watch的key加入到了watch的資料結構中,可以想到觸發watch的時機應該是修改key的內容時,通知到所有watch了該key的客戶端。

感興趣的使用者可以任意選一個修改命令跟蹤一下原始碼,例如set命令,我們發現所有對key進行修改的命令最後都會呼叫touchWatchedKey()函式,而該函式原始碼就位於multi.c檔案中,該函式就是觸發watch機制的關鍵函式,原始碼如下:

//這裡入參db就是客戶端物件中的db,上文已經提到,不贅述
void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;
    //儲存watchkey的字典為空,則返回
    if (dictSize(db->watched_keys) == 0) return;
    //注意這裡操作的是字典(即雜湊表)資料結構
    clients = dictFetchValue(db->watched_keys, key);
    //如果沒有客戶端watch該key,則返回
    if (!clients) return;
    //把client賦值給li
    listRewind(clients,&li);
    //遍歷watch了該key的客戶端,修改他們的狀態
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        c->flags |= CLIENT_DIRTY_CAS;
    }
}複製程式碼

跟我們猜測的一樣,就是每當key的內容被修改時,則遍歷所有watch了該key的客戶端,設定相應的狀態為CLIENT_DIRTY_CAS。

三、redis事務輔助命令總結

上面就是redis事務命令中watch,unwatch的實現原理,其中最複雜的應該就是watch對應的那兩套資料結構了,跟之前的pub/sub類似,都是使用連結串列+雜湊表的結構儲存,另外也是通過修改客戶端的狀態位FLAG來通知客戶端。

程式碼比較多,而且C++程式碼看上去會比較費勁,需要慢慢讀,反覆讀。

相關文章