這周學習了一下redis事務功能的實現原理,原本是想用一篇文章進行總結的,寫完之後發現這塊內容比較多,並且多個命令之間又互相依賴,放在一篇文章裏一方面篇幅會比較大,另外一方面文章組織結構會比較亂,不容易閱讀。所以把事務這個模塊整理成上下兩篇文章進行總結。redis
原文地址:http://www.jianshu.com/p/acb9...數據結構
這篇文章咱們重點分析一下redis事務命令中的兩個輔助命令:watch跟unwatch。ide
依然從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},
watch,用於客戶端關注某個key,當這個key的值被修改時,整個事務就會執行失敗(注:該命令須要在事務開啓前使用)。源碼分析
unwatch,用於客戶端取消已經watch的key。學習
用法舉例以下:
clientAspa
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>
clientB3d
127.0.0.1:6379> set a aa OK 127.0.0.1:6379>
在看具體執行函數以前首先了解幾個數據結構:指針
//每一個客戶端對象中有一個watched_keys鏈表來保存已經watch的key typedef struct client { list *watched_keys; } //上述鏈表中每一個節點的數據結構 typedef struct watchedKey { //watch的key robj *key; //指向的DB,後面細說 redisDb *db; } watchedKey;
關於事務的幾個命令所對應的函數都放在了multi.c文件中。
一塊兒看下watch命令對應處理函數的源碼:code
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的數據結構比較複雜,我這裏畫了一張圖方便理解:
簡單解釋一下上面的圖,首先redis把每一個客戶端鏈接包裝成了一個client對象,上圖中db,watch_keys就是其中的兩個字段(client對象裏面還有不少其餘字段,包括上篇文章中提到的pub/sub)。
db字段指向給該client對象分配的儲存空間,db對象中也含有一個watched_keys字段,是字典類型(也就是哈希表),以想要watch的key作key,存儲的鏈表則是全部watch該key的客戶端。
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事務命令中watch,unwatch的實現原理,其中最複雜的應該就是watch對應的那兩套數據結構了,跟以前的pub/sub相似,都是使用鏈表+哈希表的結構存儲,另外也是經過修改客戶端的狀態位FLAG來通知客戶端。
代碼比較多,並且C++代碼看上去會比較費勁,須要慢慢讀,反覆讀。