Redis源码剖析(六)事务模块
Redis允許客戶端開啟事務(wù)模式,在事務(wù)模式中,客戶端輸入的命令不會(huì)立即執(zhí)行而是被保存在事務(wù)隊(duì)列中,只有當(dāng)客戶端輸入事務(wù)運(yùn)行命令時(shí),Redis才會(huì)將事務(wù)隊(duì)列中的所有命令按照FIFO的順序一個(gè)個(gè)執(zhí)行
一個(gè)事務(wù)從開始到結(jié)束通常會(huì)經(jīng)歷三個(gè)階段
- 事務(wù)開始
- 命令入隊(duì)
- 事務(wù)執(zhí)行
事務(wù)命令
客戶端可以使用MULTI命令開啟事務(wù),隨后服務(wù)器會(huì)根據(jù)這個(gè)客戶端輸入的不同命令執(zhí)行不同的操作
- 如果客戶端發(fā)送的命令為EXEC,DISCARD,WATCH,MULTI四個(gè)命令中的一個(gè),那么服務(wù)器立刻執(zhí)行這個(gè)命令,同時(shí)是否關(guān)閉事務(wù)取決于每個(gè)命令的功能
- 如果客戶端發(fā)送的命令為上述四個(gè)命令之外的其他命令,那么服務(wù)器不會(huì)立刻執(zhí)行輸入的命令,而是將命令存放在事務(wù)隊(duì)列中
存儲(chǔ)結(jié)構(gòu)
要想當(dāng)輸入EXEC時(shí)一次性執(zhí)行之前輸入的所有命令,就需要將之前的命令保存起來(lái),Redis采用數(shù)組保存所有命令信息,在client的定義中可以找到
//server.h typedef struct client {...multiState mstate; /* 事務(wù)屬性,保存事務(wù)隊(duì)列以及事務(wù)狀態(tài) */... } client;multiState是事務(wù)屬性結(jié)構(gòu),它保存著事務(wù)隊(duì)列以及事務(wù)隊(duì)列中命令個(gè)數(shù),定義如下
//server.h /* 事務(wù)屬性 */ typedef struct multiState {multiCmd *commands; /* 事務(wù)隊(duì)列,保存多條命令 */ int count; /* 事務(wù)隊(duì)列中命令個(gè)數(shù) */ ... } multiState;commands是一個(gè)事務(wù)隊(duì)列,實(shí)際上是一個(gè)multiCmd類型的數(shù)組,multiCmd結(jié)構(gòu)保存一條命令的信息
//server.h /* 保存一條命令的信息 */ typedef struct multiCmd {robj **argv; /* 命令關(guān)鍵字和參數(shù) */int argc; /* 命令參數(shù)個(gè)數(shù) */struct redisCommand *cmd; /* 命令結(jié)構(gòu),主要包含命令處理函數(shù) */ } multiCmd;可以看到,multiCmd中保存的實(shí)際上就是執(zhí)行一條命令需要的三個(gè)信息,分別是
- 命令關(guān)鍵字和參數(shù)
- 參數(shù)個(gè)數(shù)
- 命令處理函數(shù)
在客戶端client的定義中也可以找到這三個(gè)變量的定義
//server.h typedef struct client {...int argc; robj **argv; struct redisCommand *cmd; ... } client;所以大體可以猜測(cè),當(dāng)要執(zhí)行事務(wù)隊(duì)列中的命令時(shí),只需要遍歷事務(wù)隊(duì)列,依次將這三個(gè)變量賦值給client中的對(duì)應(yīng)變量,然后調(diào)用對(duì)應(yīng)命令處理函數(shù)即可。后面會(huì)看到,事實(shí)也正是如此
事務(wù)實(shí)現(xiàn)
開啟事務(wù)
在Redis內(nèi)部,事務(wù)的開啟十分簡(jiǎn)單,僅僅是將客戶端的事務(wù)標(biāo)志位打開,表示進(jìn)行事務(wù)狀態(tài),隨后的大多數(shù)操作都會(huì)先判斷該標(biāo)志位以確定是將命令添加到事務(wù)隊(duì)列中,還是執(zhí)行命令
//multi.c /* 開啟事務(wù) */ void multiCommand(client *c) {/* 若已經(jīng)開啟,則報(bào)錯(cuò) */if (c->flags & CLIENT_MULTI) {addReplyError(c,"MULTI calls can not be nested");return;}/* 設(shè)置CLIENT_MULTI標(biāo)志代表客戶端已經(jīng)開啟事務(wù) */c->flags |= CLIENT_MULTI;addReply(c,shared.ok); }添加命令到事務(wù)隊(duì)列
在第一篇服務(wù)器與客戶端交互流程中得知,當(dāng)客戶端輸入命令后,會(huì)存在一個(gè)解析命令的操作,將命令參數(shù),參數(shù)個(gè)數(shù)以及命令處理函數(shù)找到,然后執(zhí)行call函數(shù),在這個(gè)函數(shù)中調(diào)用命令處理函數(shù)執(zhí)行命令。但是一旦開啟事務(wù)功能,Redis就不能再執(zhí)行命令了,如上所述,應(yīng)該將命令添加到事務(wù)隊(duì)列中。
在processCommand函數(shù)中,可以看到對(duì)于這兩種情況的判斷
/* 處理客戶端輸入的命令 */ int processCommand(client *c) {.../* 從命令字典中查找該命令名字,返回redisCommand結(jié)構(gòu),其中包含命令處理函數(shù) */c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);.../* 如果客戶端開啟事務(wù),則不執(zhí)行命令而是將命令添加到事務(wù)隊(duì)列中 */if (c->flags & CLIENT_MULTI &&c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&c->cmd->proc != multiCommand && c->cmd->proc != watchCommand){queueMultiCommand(c);/* 回復(fù)客戶端當(dāng)前命令已經(jīng)添加到事務(wù)隊(duì)列中 */addReply(c,shared.queued);} else {/* 沒(méi)有開啟事務(wù),執(zhí)行執(zhí)行命令 */call(c,CMD_CALL_FULL);...}return C_OK; }將命令添加到事務(wù)隊(duì)列中由queueMultiCommand函數(shù)完成,函數(shù)首先創(chuàng)建一個(gè)multiCmd對(duì)象,這個(gè)結(jié)構(gòu)在上面提到過(guò),保存著執(zhí)行一條命令需要的三個(gè)元素,分別是命令參數(shù),參數(shù)個(gè)數(shù)以及命令處理函數(shù)。而multiState結(jié)構(gòu)是保存事務(wù)隊(duì)列的結(jié)構(gòu)(實(shí)際是數(shù)組),在這里需要將新的命令添加到這個(gè)數(shù)組中
/* 將當(dāng)前命令添加到客戶端的事務(wù)隊(duì)列中 */ void queueMultiCommand(client *c) {multiCmd *mc;int j;/* mstate是multiState類型,保存事務(wù)中所有命令的信息* 每增加一條命令到事務(wù)隊(duì)列中,都需要為原事務(wù)隊(duì)列重新申請(qǐng)n+1大小的空間* 多的那一個(gè)用來(lái)存儲(chǔ)當(dāng)前命令*/c->mstate.commands = zrealloc(c->mstate.commands,sizeof(multiCmd)*(c->mstate.count+1));mc = c->mstate.commands+c->mstate.count;/* 保存執(zhí)行一條命令所需的三個(gè)元素 */mc->cmd = c->cmd;mc->argc = c->argc;mc->argv = zmalloc(sizeof(robj*)*c->argc);/* 將參數(shù)復(fù)制到multiCmd結(jié)構(gòu)中 */memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);/* 因?yàn)閰?shù)是robj*類型,所以引用計(jì)數(shù)加一 */for (j = 0; j < c->argc; j++)incrRefCount(mc->argv[j]);/* 事務(wù)隊(duì)列中命令個(gè)數(shù)加一 */c->mstate.count++; }執(zhí)行命令
當(dāng)客戶端輸入EXEC命令后,Redis會(huì)從客戶端的事務(wù)隊(duì)列中取出命令,按照保存的先后順序一個(gè)個(gè)執(zhí)行,由于事務(wù)隊(duì)列中有命令的所有信息,所以可以執(zhí)行調(diào)用處理函數(shù)。這部分操作由execCommand函數(shù)執(zhí)行,因?yàn)閳?zhí)行一條命令是從客戶端對(duì)象client中取出命令參數(shù),參數(shù)個(gè)數(shù)以及命令處理函數(shù),所以這里就直接將隊(duì)列中的命令信息復(fù)制給客戶端對(duì)象的對(duì)應(yīng)變量,然后和執(zhí)行正常命令一樣調(diào)用call函數(shù)
/* 啟動(dòng)事務(wù)命令 */ void execCommand(client *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? *//* CLIENT_MULTI標(biāo)識(shí)代表當(dāng)前客戶端是否開啟事務(wù),如果沒(méi)有開啟,執(zhí)行EXEC指令是沒(méi)有意義的 */if (!(c->flags & CLIENT_MULTI)) {addReplyError(c,"EXEC without MULTI");return;}/* CLIENT_DIRTY_CAS標(biāo)識(shí)代表客戶端監(jiān)視的鍵是否被修改過(guò)* 如果被修改過(guò),那么執(zhí)行事務(wù)就不再安全,直接返回 */if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :shared.nullmultibulk);discardTransaction(c);goto handle_monitor;}/* Exec all the queued commands *//* 開始執(zhí)行事務(wù),監(jiān)視任務(wù)就可以結(jié)束了,將該客戶端的監(jiān)視字典清空 */unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles *//* 臨時(shí)保存客戶端當(dāng)前參數(shù)信息 */orig_argv = c->argv;orig_argc = c->argc;orig_cmd = c->cmd;addReplyMultiBulkLen(c,c->mstate.count);/* 開始執(zhí)行客戶端數(shù)據(jù)隊(duì)列(數(shù)組)中的命令 */for (j = 0; j < c->mstate.count; j++) {/* 將每個(gè)命令作為客戶端當(dāng)前的參數(shù)信息(這就是為什么需要臨時(shí)保存以前參數(shù)的原因) */c->argc = c->mstate.commands[j].argc;c->argv = c->mstate.commands[j].argv;c->cmd = c->mstate.commands[j].cmd;.../* 開始執(zhí)行命令 */call(c,CMD_CALL_FULL);/* Commands may alter argc/argv, restore mstate. *//* call執(zhí)行的命令可能會(huì)改變事務(wù)隊(duì)列中的當(dāng)前命令,這里是確保其不被改變 */c->mstate.commands[j].argc = c->argc;c->mstate.commands[j].argv = c->argv;c->mstate.commands[j].cmd = c->cmd;}/* 還原客戶端以前的參數(shù)信息 */c->argv = orig_argv;c->argc = orig_argc;c->cmd = orig_cmd;... }函數(shù)有點(diǎn)長(zhǎng),不過(guò)還算好理解,先判斷客戶端是否開啟事務(wù)(利用CLIENT_MULTI標(biāo)記),然后清空客戶端的監(jiān)視鏈表(后面會(huì)提到),最后遍歷事務(wù)隊(duì)列,依次執(zhí)行每個(gè)命令
需要注意的是,Redis在執(zhí)行事務(wù)隊(duì)列中的命令時(shí)保證即使某條命令是錯(cuò)誤的,也不會(huì)影響到其他命令的執(zhí)行,即如果中間某條命令執(zhí)行出錯(cuò),那么后面的命令仍然會(huì)繼續(xù)執(zhí)行。另外,Redis不支持事務(wù)回滾功能,即不支持將已執(zhí)行的命令撤銷
小結(jié)
Redis的事務(wù)功能還是比較簡(jiǎn)單的,另外需要提的是,由于Redis是運(yùn)行在單線程下的,而且對(duì)于客戶端的監(jiān)聽是基于io多路復(fù)用函數(shù)的,所以對(duì)于客戶端的響應(yīng)是串行的,不會(huì)出現(xiàn)當(dāng)執(zhí)行某個(gè)客戶端事務(wù)隊(duì)列中的命令時(shí)切換到另一個(gè)客戶端的情況。這保證了事務(wù)執(zhí)行具有原子性,另外Redis設(shè)計(jì)與實(shí)現(xiàn)書中還講到Redis的事務(wù)具有一致性,隔離性和耐久性,有興趣的話可以翻閱書籍查看
總結(jié)
以上是生活随笔為你收集整理的Redis源码剖析(六)事务模块的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 每天一道LeetCode-----为二叉
- 下一篇: Redis源码剖析(七)监视功能