15天玩轉redis —— 第八篇 你不得不會的事務玩法

一線碼農發表於2015-11-24

   我們都知道redis追求的是簡單,快速,高效,在這種情況下也就拒絕了支援window平臺,學sqlserver的時候,我們知道事務還算是個比較複雜的東西,

所以這吊毛要是照搬到redis中去,理所當然redis就不是那麼簡單純碎的東西了,但是呢,事務是我們寫程式無法逃避的場景,所以redis作者折衷的寫了個簡

化版的事務機制,下面我來扯一下它的蛋蛋。

 

一: 事務實戰

  具體到事務是什麼,要保證什麼。。。這個我想沒必要說了,先不管三七二十一,看一下redis手冊,領略下它的魔力。

1. multi,exec

   還記得sqlserver是怎麼玩的嗎?一般都是這樣的三個步驟,生成事務,產生命令,執行事務,對吧,而對應redis呢??multi就是生成事務,然後

輸入redis命令,最後用exec執行命令,就像下面這樣:

可以看到,我set完命令之後,反饋資訊是QUEUED,最後我再執行exec,這些命令才會真正的執行,就是這麼的簡單,一切執行的就是那麼的順利,

一點都不拖泥帶水,牛逼的不要不要的,可能有些人說,其實事務中還有一個rollback操作,但好像在redis中沒有看到,哈哈,牛逼哈,很遺憾是

redis中沒有rollback操作,比如下面這樣。

 

在圖中我故意用lpush命令去執行string,可想而知自然不會執行成功,但從結果中,你看到什麼了呢?兩個OK,一個Error,這就是違反了事務

的原子性,對吧,但是我該怎麼反駁呢??? 我會說,錯你妹啊。。。連個基本的命令都寫錯了,你搞個毛啊。。。還寫個吊毛程式碼,reids僅僅

是個資料結構伺服器,多簡單的一件事情,退一萬步說,很明顯的錯誤命令它會直接返回的,比如我故意把lpush寫成lpush1:

 

2. watch

  不知道你看完multi後面的三條set命令之後,有沒有一種心虛的感覺,怎麼說呢,就是隻要命令是正確的,redis保證會一併執行,誓死完成

任務,雖然說命令是一起執行的,但是誰可以保證我在執行命令的過程中,其他client不會修改這些值呢???如果修改了這些值,那我的exec

還有什麼意義呢???沒關係,這種爛大街的需求,redis怎可能袖手旁觀???這裡的watch就可以助你一臂之力。

WATCH
WATCH key [key ...]

監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那麼事務將被打斷。

 上面就是redis手冊中關於watch的解釋,使用起來貌似很簡單,就是我在multi之前,用watch去監視我要修改的key,如果說我在exec之前,

multi之後的這段時間,key被其他client修改,那麼exec就會執行失敗,返回(nil),就這麼簡單,我還是來舉個例子:

 

 

二:原理探索

  關於事務操作的原始碼,大多都在redis原始碼中的multi.c 檔案中,接下來我會一個一個的簡單剖析一下:

1. multi

  在redis的原始碼中,它大概是這麼寫的:

1 void multiCommand(redisClient *c) {
2     if (c->flags & REDIS_MULTI) {
3         addReplyError(c,"MULTI calls can not be nested");
4         return;
5     }
6     c->flags |= REDIS_MULTI;
7     addReply(c,shared.ok);
8 }

從這段程式碼中,你可以看到multi只是簡單的把redisClient的REDIS_MULTI狀態開啟,告訴這個redis客戶端已經進入事務模式了,對吧。

 

2. 生成命令

在redisClient中,裡面有一個multiState命令:

typedef struct redisClient {
    。。。
    multiState mstate;      /* MULTI/EXEC state */
    。。。
} redisClient;

從註釋中你大概也看到了這個命令和multi/exec肯定有關係,接下來我很好奇的看看multiState的定義:

typedef struct multiState {
    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;

從multiState這個列舉中,你可以看到下面有一個*command命令,從註釋中可以看到它其實指向的是一個陣列,這個陣列我想你閉著眼睛都

能想得到吧。。。它就是你的若干條命令啦。。。下面還有一個count,可以看到是實際的commands的總數。

 

3. watch

    為了方便說到後面的exec,這裡想說一下watch大概是怎麼實現的,在multi.c原始碼中是這樣寫的。

 1 typedef struct watchedKey {
 2     robj *key;
 3     redisDb *db;
 4 } watchedKey;
 5 
 6 void watchCommand(redisClient *c) {
 7     int j;
 8 
 9     if (c->flags & REDIS_MULTI) {
10         addReplyError(c,"WATCH inside MULTI is not allowed");
11         return;
12     }
13     for (j = 1; j < c->argc; j++)
14         watchForKey(c,c->argv[j]);
15     addReply(c,shared.ok);
16 }
17 
18 /* Watch for the specified key */
19 void watchForKey(redisClient *c, robj *key) {
20     list *clients = NULL;
21     listIter li;
22     listNode *ln;
23     watchedKey *wk;
24 
25     /* Check if we are already watching for this key */
26     listRewind(c->watched_keys,&li);
27     while((ln = listNext(&li))) {
28         wk = listNodeValue(ln);
29         if (wk->db == c->db && equalStringObjects(key,wk->key))
30             return; /* Key already watched */
31     }
32     /* This key is not already watched in this DB. Let's add it */
33     clients = dictFetchValue(c->db->watched_keys,key);
34     if (!clients) {
35         clients = listCreate();
36         dictAdd(c->db->watched_keys,key,clients);
37         incrRefCount(key);
38     }
39     listAddNodeTail(clients,c);
40     /* Add the new key to the list of keys watched by this client */
41     wk = zmalloc(sizeof(*wk));
42     wk->key = key;
43     wk->db = c->db;
44     incrRefCount(key);
45     listAddNodeTail(c->watched_keys,wk);
46 }

這段程式碼中大概最核心的一點就是:

    /* This key is not already watched in this DB. Let's add it */
    clients = dictFetchValue(c->db->watched_keys,key);

就是通過dicFetchValue這個字典方法,從watched_keys中找到指定key的value,而這個value是一個clients的連結串列,說明人家其實是想找到

關於這個key的所有client,對吧,最後還會將本次key塞入到redisclient的watched_keys字典中,如下程式碼:

    /* Add the new key to the list of keys watched by this client */
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);

如果非要畫圖,大概就是這樣:

其中watched_key是個字典結構,字典的鍵為上面的key1,key2。。。,value為client的連結串列,這樣的話,我就非常清楚某個key

中是被哪些client監視著的,對吧。

 

4.exec

    這個命令裡面大概做了兩件事情:

<1>:   判斷c->flags=REDIS_DIRTY_EXEC 開啟與否,如果是的話,取消事務discardTransaction(c),也就是說這個key已經

          被的client修改了。

<2>:   如果沒有修改,那麼就for迴圈執行comannd[]中的命令,如下圖中的兩處資訊:

  

 

好了,大概就這麼說了,希望對你有幫助哈~~~

 

相關文章