我們都知道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[]中的命令,如下圖中的兩處資訊:
好了,大概就這麼說了,希望對你有幫助哈~~~