Redis源碼學(xué)習(xí)之Redis事務(wù)
Redis作為一個(gè)內(nèi)存型數(shù)據(jù)庫(kù),同樣支持傳統(tǒng)數(shù)據(jù)庫(kù)的事務(wù)特性。這篇文章會(huì)從源代碼角度來(lái)分析Redis中事務(wù)的實(shí)現(xiàn)原理。
What
Redis事務(wù)提供了一種將多個(gè)命令請(qǐng)求打包,然后一次性、按照順序地執(zhí)行多個(gè)命令的機(jī)制,并且在事務(wù)執(zhí)行的期間,服務(wù)器不會(huì)中斷事務(wù)而去執(zhí)行其他不在事務(wù)中的命令請(qǐng)求,它會(huì)把事務(wù)中所有的命令都執(zhí)行完畢才會(huì)去執(zhí)行其他的命令。
How
Redis中提供了multi、discard、exec、watch、unwatch這幾個(gè)命令來(lái)實(shí)現(xiàn)事務(wù)的功能。
Redis的事務(wù)始于multi命令,之后跟著要在事務(wù)中執(zhí)行的命令,終于exec命令或者discard命令。加入事務(wù)中的所有命令會(huì)原子的執(zhí)行,中間不會(huì)穿插執(zhí)行其他沒(méi)有加入事務(wù)的命令。
multi、exec和discard
multi命令告訴Redis客戶端要開始一個(gè)事物,然后Redis會(huì)返回一個(gè)OK,接下來(lái)所有的命令Redis都不會(huì)立即執(zhí)行,只會(huì)返回QUEUED結(jié)果,直到遇到了exec命令才會(huì)去執(zhí)行之前的所有的命令,或者遇到了discard命令,會(huì)拋棄執(zhí)行之前加入事務(wù)的命令。
- 127.0.0.1:6379> get name
- (nil)
- 127.0.0.1:6379> get gender
- (nil)
- 127.0.0.1:6379> multi
- OK
- 127.0.0.1:6379> set name Slogen
- QUEUED
- 127.0.0.1:6379> set gender male
- QUEUED
- 127.0.0.1:6379> exec
- 1) OK
- 2) OK
- 127.0.0.1:6379> mget name gender
- 1) "Slogen"
- 2) "male"
watch
watch命令是Redis提供的一個(gè)樂(lè)觀鎖,可以在exec執(zhí)行之前,監(jiān)視任意數(shù)量的數(shù)據(jù)庫(kù)key,并在exec命令執(zhí)行的時(shí)候,檢測(cè)被監(jiān)視的key是否至少有一個(gè)已經(jīng)被修改,如果是的話,服務(wù)器將拒絕執(zhí)行事務(wù),并向客戶端返回代表事務(wù)執(zhí)行失敗的空回復(fù)。
首先在client1執(zhí)行下列命令:
- 127.0.0.1:6379> get name
- (nil)
- 127.0.0.1:6379> watch name
- OK
- 127.0.0.1:6379> multi
- OK
- 127.0.0.1:6379> set name slogen
- QUEUED
- 127.0.0.1:6379> set gender male
- QUEUED
- 127.0.0.1:6379> get name
- QUEUED
這個(gè)時(shí)候client還沒(méi)有執(zhí)行exec命令,接下來(lái)在client2下執(zhí)行下面命令修改name:
- 127.0.0.1:6379> set name rio
- OK
- 127.0.0.1:6379> get name
- "rio"
接下來(lái)在client1下執(zhí)行exec命令:
- 127.0.0.1:6379> exec
- (nil)
- 127.0.0.1:6379> get name
- "rio"
從執(zhí)行結(jié)果可以看到,在client1中執(zhí)行exec命令的時(shí)候,Redis會(huì)檢測(cè)到name字段已經(jīng)被其他客戶端修改了,所以拒絕執(zhí)行事務(wù)中所有的命令,直接返回nil表示執(zhí)行失敗。這個(gè)時(shí)候獲取到的name的值還是在client2中設(shè)置的rio。
Why
multi
Redis的事務(wù)始于multi命令,那么就從multi命令的源代碼開始分析。
當(dāng)Redis接收到客戶端發(fā)送過(guò)來(lái)的命令之后會(huì)執(zhí)行multiCommand()這個(gè)方法,這個(gè)方法在multi.c文件中。
- void multiCommand(client *c) {
- // 1. 如果檢測(cè)到flags里面已經(jīng)包含了CLIENT_MULTI
- // 表示對(duì)應(yīng)client已經(jīng)處于事務(wù)的上下文中,返回錯(cuò)誤
- if (c->flags & CLIENT_MULTI) {
- addReplyError(c,"MULTI calls can not be nested");
- return;
- }
- // 2. 開啟flags的CLIENT_MULTI標(biāo)識(shí)
- c->flags |= CLIENT_MULTI;
- // 3. 返回ok,告訴客戶端已經(jīng)成功開啟事務(wù)
- addReply(c,shared.ok);
- }
從源代碼中可以看到,multiCommand()主要完成下面三件事:
- 檢測(cè)發(fā)送multi命令的client是否已經(jīng)處于事務(wù)中,如果是則直接返回錯(cuò)誤。從這里可以看到,Redis不支持事務(wù)嵌套執(zhí)行。
- 給對(duì)應(yīng)client的flags標(biāo)志位中增加MULTI_CLIENT標(biāo)志,表示已經(jīng)進(jìn)入事務(wù)中。
- 返回OK告訴客戶端已經(jīng)成功開啟事務(wù)。
從前面的文章中可以知道,Redis接收到所有的Client發(fā)送過(guò)來(lái)的命令后都會(huì)執(zhí)行到processCommand()這個(gè)方法中,在processCommand()中有下面這部分代碼:
在processCommand()執(zhí)行實(shí)際的命令之前會(huì)先判斷對(duì)應(yīng)的client是否已經(jīng)處于事務(wù)的上下文中,如果是的話,且需要執(zhí)行的命令不是exec、discard、multi和watch這四個(gè)命令中的任何一個(gè),則調(diào)用queueMultiCommand()方法把需要執(zhí)行的命令加入隊(duì)列中,否則的話調(diào)用call()直接執(zhí)行命令。
queueMultiCommand()
Redis調(diào)用queueMultiCommand()方法把加入事務(wù)的命令加入Redis隊(duì)列中,實(shí)現(xiàn)如下:
queueMultiCommand()方法主要是把要加入事務(wù)的命令封裝在multiCmd結(jié)構(gòu)的變量,然后放置到client->mstate.commands數(shù)組中去,multiCmd的定義如下:
- typedef struct multiCmd {
- robj **argv; // 命令的參數(shù)數(shù)組
- int argc; // 命令的參數(shù)個(gè)數(shù)
- struct redisCommand *cmd; // 要執(zhí)行的命令
- } multiCmd;
而mstate字段定義為:
- typedef struct client {
- // 其他省略代碼
- multiState mstate; /* MULTI/EXEC state */
- } client;
multiState的結(jié)構(gòu)為:
- 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;
- commands:multiCmd類型的數(shù)組,存放著事務(wù)中所有的要執(zhí)行的命令
- count:當(dāng)前事務(wù)中所有已經(jīng)存放的命令的個(gè)數(shù)
另外兩個(gè)字段當(dāng)前版本中(3.2.28)沒(méi)用上。
假設(shè)當(dāng)前事務(wù)隊(duì)列中已經(jīng)存在set name slogen和lpush num 20這兩個(gè)命令的時(shí)候,client中的mstate的數(shù)據(jù)如下:
這個(gè)時(shí)候再往事務(wù)中添加get name這個(gè)命令的時(shí)候結(jié)構(gòu)圖如下:
錯(cuò)誤命令:CLIENT_DIRTY_EXEC
那么有個(gè)問(wèn)題,比如我往事務(wù)中添加的命令是個(gè)不存在的命令,或者命令使用方式,比如命令參數(shù)不對(duì),這個(gè)時(shí)候這個(gè)命令會(huì)被加入事務(wù)嗎?
前面說(shuō)了,Redis接收到的所有的命令都是執(zhí)行到processCommand()這個(gè)方法,在實(shí)際執(zhí)行對(duì)應(yīng)的命令前,processCommand()方法都會(huì)對(duì)將要執(zhí)行的命令進(jìn)行一系列的檢查,代碼如下:
從上面代碼可以看到,processCommand()在對(duì)要執(zhí)行的命令進(jìn)行的一系列檢查的時(shí)候如果有任何一項(xiàng)檢測(cè)失敗都會(huì)調(diào)用flagTransaction()函數(shù)然后返回對(duì)應(yīng)的信息給客戶端,flagTransaction()實(shí)現(xiàn)如下:
- void flagTransaction(client *c) {
- if (c->flags & CLIENT_MULTI)
- // 如果flags包含CLIENT_MULTI標(biāo)志位,表示已經(jīng)處于事務(wù)上下文中
- // 則給對(duì)應(yīng)的client的flags開啟CLIENT_DIRTY_EXEC標(biāo)志位
- c->flags |= CLIENT_DIRTY_EXEC;
- }
flagTransaction()方法會(huì)檢測(cè)對(duì)應(yīng)的client是否處于事務(wù)的上下文中,如果是的話就給對(duì)應(yīng)的client的flags字段開啟CLIENT_DIRTY_EXEC標(biāo)志位。
也就是說(shuō),如果命令在加入事務(wù)的時(shí)候由于各種原因,比如命令不存在,或者對(duì)應(yīng)的命令參數(shù)不正確,則對(duì)應(yīng)的命令不會(huì)被添加到mstate.commands數(shù)組中,且同時(shí)給對(duì)應(yīng)的client的flags字段開啟CLIENT_DIRTY_EXEC標(biāo)志位。
watch命令
當(dāng)client處于事務(wù)的上下文中時(shí),watch命令屬于可以被立即執(zhí)行的幾個(gè)命令之一,watch命令對(duì)應(yīng)的代碼為watchCommand()函數(shù),實(shí)現(xiàn)如下:
- void watchCommand(client *c) {
- int j;
- if (c->flags & CLIENT_MULTI) {
- // 如果執(zhí)行watch命令的client處于事務(wù)的上下文中則直接返回
- addReplyError(c,"WATCH inside MULTI is not allowed");
- return;
- }
- for (j = 1; j < c->argc; j++)
- // 對(duì)傳入的每個(gè)要watch的可以調(diào)用watchForKey()
- watchForKey(c,c->argv[j]);
- addReply(c,shared.ok);
- }
watchCommand()方法會(huì)首先判斷執(zhí)行watch的命令是否已經(jīng)處于事務(wù)的上下文中,如果是的話則直接報(bào)錯(cuò)返回,說(shuō)明在Redis事務(wù)中不能調(diào)用watch命令。
接下來(lái)對(duì)于watch命令傳入的所有的key,依次調(diào)用watchForKey()方法,定義如下:
watchForKey()方法會(huì)做下面幾件事:
- 判斷對(duì)應(yīng)的key是否已經(jīng)存在于client->watched_keys列表中,如果已經(jīng)存在則直接返回。client->watched_keys保存著對(duì)應(yīng)的client對(duì)象所有的要監(jiān)視的key。
- 如果不存在,則去client->db->watched_keys中查找所有的已經(jīng)監(jiān)視了這個(gè)key的client對(duì)象。client->db->watched_keys以dict的結(jié)構(gòu)保存了所有的監(jiān)視這個(gè)key的client列表。
- 如果第二步中的列表存在,則把執(zhí)行watch命令的client添加到這個(gè)列表的尾部,如果不存在,表示還沒(méi)有任何一個(gè)client監(jiān)視這個(gè)key,則新建一個(gè)列表,添加到client->db->watched_keys中,然后把執(zhí)行watch命令的client添加到新生成的列表的尾部。
- 把傳入的key封裝成一個(gè)watchedKey結(jié)構(gòu)的變量,添加到client->watched_key列表的最后面。
假設(shè)當(dāng)前client->db->watched_keys的監(jiān)測(cè)情況如下圖所示:
而client->watched_keys的監(jiān)測(cè)情況如下:
這個(gè)時(shí)候client_A執(zhí)行watch key1 key2 key3這個(gè)命令,執(zhí)行完命令之后client->db->watched_keys結(jié)果為
而client->watched_keys結(jié)果為

對(duì)于key1,目前還沒(méi)有client對(duì)key1進(jìn)行監(jiān)視,所以這個(gè)時(shí)候client_A會(huì)新建一個(gè)列表,把自己添加到這個(gè)列表中然后把映射關(guān)系添加到client->db->watched_keys中去,之后會(huì)把key1添加到client->watched_keys列表的最后。
對(duì)于key2,由于已經(jīng)存在于watched_keys列表中,所以會(huì)直接返回不做任何處理。
對(duì)于key3,由于client->db->watched_keys中已經(jīng)有client_B和client_C在監(jiān)視它,所以會(huì)直接把client_A添加到監(jiān)視列表的末尾之后再把key3添加到client_A的監(jiān)視列表中去。
修改數(shù)據(jù):CLIENT_DIRTY_CAS
watch命令的作用就是用在事務(wù)中檢測(cè)被監(jiān)視的key是否被其他的client修改了,如果已經(jīng)被修改,則阻止事務(wù)的執(zhí)行,那么這個(gè)功能是怎么實(shí)現(xiàn)的呢?
這里以set命令為例進(jìn)行分析。
假設(shè)client_A執(zhí)行了watch name這個(gè)命令然后執(zhí)行multi命令開啟了事務(wù)但是還沒(méi)有執(zhí)行exec命令,這個(gè)時(shí)候client_B執(zhí)行了set name slogen這個(gè)命令,整個(gè)過(guò)程如下:
時(shí)間 | client_A | client_B |
---|---|---|
T1 | watch name | |
T2 | multi | |
T3 | get name | |
T4 | set name slogen | |
T5 | exec |
在T4的時(shí)候client_B執(zhí)行了set命令修改了name,Redis收到set命令之后會(huì)執(zhí)行setCommand方法,實(shí)現(xiàn)如下:
- void setCommand(client *c) {
- // 其他省略代碼
- setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
- }
在setCommand()最后會(huì)調(diào)用setGenericCommand()方法,改方法實(shí)現(xiàn)如下:
- void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
- // 其他省略代碼
- setKey(c->db,key,val);
- // 其他省略代碼
- }
在setGenericCommand()方法中會(huì)調(diào)用setKey()這個(gè)方法,接著看下setKey()這個(gè)方法:
- void setKey(redisDb *db, robj *key, robj *val) {
- if (lookupKeyWrite(db,key) == NULL) {
- dbAdd(db,key,val);
- } else {
- dbOverwrite(db,key,val);
- }
- incrRefCount(val);
- removeExpire(db,key);
- // 通知修改了key
- signalModifiedKey(db,key);
- }
在setKey()方法最后會(huì)調(diào)用signaleModifiedKey()通知redis數(shù)據(jù)庫(kù)中有數(shù)據(jù)被修改,signaleModifiedKey()方法實(shí)現(xiàn)如下:
- void signalModifiedKey(redisDb *db, robj *key) {
- touchWatchedKey(db,key);
- }
可以看到signalModifiedKey()也僅僅是調(diào)用touchWatchedKey()方法,代碼如下:
- void touchWatchedKey(redisDb *db, robj *key) {
- list *clients;
- listIter li;
- listNode *ln;
- if (dictSize(db->watched_keys) == 0) return;
- // 1. 從redisDb->watched_keys中找到對(duì)應(yīng)的client列表
- clients = dictFetchValue(db->watched_keys, key);
- if (!clients) return;
- /* Mark all the clients watching this key as CLIENT_DIRTY_CAS */
- /* Check if we are already watching for this key */
- listRewind(clients,&li);
- while((ln = listNext(&li))) {
- // 2.依次遍歷client列表,給每個(gè)client的flags字段
- // 開啟CLIENT_DIRTY_CAS標(biāo)識(shí)位
- client *c = listNodeValue(ln);
- c->flags |= CLIENT_DIRTY_CAS;
- }
- }
touchWatchedKey()方法會(huì)做下面兩件事:
- 從redisDb->watched_keys中找到監(jiān)視這個(gè)key的client列表。前面在分析watch命令的時(shí)候說(shuō)過(guò),如果有client執(zhí)行了watch keys命令,那么redis會(huì)以鍵值對(duì)的形式把(key,client)的對(duì)應(yīng)關(guān)系保存在redisDb->watched_key這個(gè)字段里面。
- 對(duì)于第一步中找到的每個(gè)client對(duì)象,都會(huì)給這個(gè)client的flags 字段開啟CLIENT_DIRTY_CAS標(biāo)志位。
在Redis里面所有會(huì)修改數(shù)據(jù)庫(kù)內(nèi)容的命令最后都會(huì)調(diào)用signalModifiedKey()這個(gè)方法,而在signalModifiedKey()會(huì)給所有的監(jiān)視這個(gè)key的client增加CLIENT_DIRTY_CAS標(biāo)志位。
exec命令
exec命令用來(lái)執(zhí)行事務(wù),對(duì)應(yīng)的代碼為execCommand()這個(gè)方法,實(shí)現(xiàn)如下:
- 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? */
- // 1. 判斷對(duì)應(yīng)的client是否屬于事務(wù)中
- if (!(c->flags & CLIENT_MULTI)) {
- addReplyError(c,"EXEC without MULTI");
- return;
- }
- /**
- * 2. 檢查是否需要執(zhí)行事務(wù),在下面兩種情況下不會(huì)執(zhí)行事務(wù)
- * 1) 有被watch的key被其他的客戶端修改了,對(duì)應(yīng)于CLIENT_DIRTY_CAS標(biāo)志位被開啟
- * ,這個(gè)時(shí)候會(huì)返回一個(gè)nil,表示沒(méi)有執(zhí)行事務(wù)
- * 2) 有命令在加入事務(wù)隊(duì)列的時(shí)候發(fā)生錯(cuò)誤,對(duì)應(yīng)于CLIENT_DIRTY_EXEC標(biāo)志位被開啟
- * ,這個(gè)時(shí)候會(huì)返回一個(gè)execaborterr錯(cuò)誤
- */
- if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
- addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :
- shared.nullmultibulk);
- // 取消所有的事務(wù)
- discardTransaction(c);
- goto handle_monitor;
- }
- /* Exec all the queued commands */
- // 3. unwatch所有被這個(gè)client watch的key
- unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
- orig_argv = c->argv;
- orig_argc = c->argc;
- orig_cmd = c->cmd;
- addReplyMultiBulkLen(c,c->mstate.count);
- // 4. 依次執(zhí)行事務(wù)隊(duì)列中所有的命令
- for (j = 0; j < c->mstate.count; j++) {
- c->argc = c->mstate.commands[j].argc;
- c->argv = c->mstate.commands[j].argv;
- c->cmd = c->mstate.commands[j].cmd;
- /* Propagate a MULTI request once we encounter the first write op.
- * This way we'll deliver the MULTI/..../EXEC block as a whole and
- * both the AOF and the replication link will have the same consistency
- * and atomicity guarantees. */
- if (!must_propagate && !(c->cmd->flags & CMD_READONLY)) {
- execCommandPropagateMulti(c);
- must_propagate = 1;
- }
- call(c,CMD_CALL_FULL);
- /* Commands may alter argc/argv, restore mstate. */
- c->mstate.commands[j].argc = c->argc;
- c->mstate.commands[j].argv = c->argv;
- c->mstate.commands[j].cmd = c->cmd;
- }
- c->argv = orig_argv;
- c->argc = orig_argc;
- c->cmd = orig_cmd;
- // 5. 重置這個(gè)client對(duì)應(yīng)的事務(wù)相關(guān)的所有的數(shù)據(jù)
- discardTransaction(c);
- /* Make sure the EXEC command will be propagated as well if MULTI
- * was already propagated. */
- if (must_propagate) server.dirty++;
- handle_monitor:
- if (listLength(server.monitors) && !server.loading)
- replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
- }
execCommand()方法會(huì)做下面幾件事:
- 判斷對(duì)應(yīng)的client是否已經(jīng)處于事務(wù)中,如果不是,則直接返回錯(cuò)誤。
- 判斷時(shí)候需要執(zhí)行事務(wù)中的命令。在下面兩種情況下不會(huì)執(zhí)行事務(wù)而是返回錯(cuò)誤。
- 有被監(jiān)視的key被其他的客戶端修改了,對(duì)應(yīng)于CLIENT_DIRTY_CAS標(biāo)志位被開啟,這個(gè)時(shí)候會(huì)返回一個(gè)nil,表示沒(méi)有執(zhí)行事務(wù)。
- 有命令在加入事務(wù)隊(duì)列的時(shí)候發(fā)生錯(cuò)誤,對(duì)應(yīng)于CLIENT_DIRTY_EXEC標(biāo)志位被開啟,這個(gè)時(shí)候會(huì)返回一個(gè)execaborterr錯(cuò)誤。
- unwatch所有被這個(gè)client監(jiān)視的key。
- 依次執(zhí)行事務(wù)隊(duì)列中所有的命令。
- 重置這個(gè)client對(duì)應(yīng)的事務(wù)相關(guān)的所有的數(shù)據(jù)。
discard
使用discard命令可以取消一個(gè)事務(wù),對(duì)應(yīng)的方法為discardCommand(),實(shí)現(xiàn)如下:
- void discardCommand(client *c) {
- // 1. 檢查對(duì)應(yīng)的client是否處于事務(wù)中
- if (!(c->flags & CLIENT_MULTI)) {
- addReplyError(c,"DISCARD without MULTI");
- return;
- }
- // 2. 取消事務(wù)
- discardTransaction(c);
- addReply(c,shared.ok);
- }
discardCommand()方法首先判斷對(duì)應(yīng)的client是否處于事務(wù)中,如果不是則直接返回錯(cuò)誤,否則的話會(huì)調(diào)用discardTransaction()方法取消事務(wù),該方法實(shí)現(xiàn)如下:
- void discardTransaction(client *c) {
- // 1. 釋放所有跟MULTI/EXEC狀態(tài)相關(guān)的資源
- freeClientMultiState(c);
- // 2. 初始化相應(yīng)的狀態(tài)
- initClientMultiState(c);
- // 3. 取消對(duì)應(yīng)client的3個(gè)標(biāo)志位
- c->flags &= ~(CLIENT_MULTI|CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC);
- // 4.unwatch所有已經(jīng)被watch的key
- unwatchAllKeys(c);
- }
Other
Atomic:原子性
原子性是指一個(gè)事務(wù)(transaction)中的所有操作,要么全部完成,要么全部不完成,不會(huì)結(jié)束在中間某個(gè)環(huán)節(jié)。
對(duì)于Redis的事務(wù)來(lái)說(shuō),事務(wù)隊(duì)列中的命令要么全部執(zhí)行完成,要么一個(gè)都不執(zhí)行,因此Redis的事務(wù)是具有原子性的。
注意Redis不提供事務(wù)回滾機(jī)制。
Consistency:一致性
事務(wù)的一致性是指事務(wù)的執(zhí)行結(jié)果必須是使事務(wù)從一個(gè)一致性狀態(tài)變到另一個(gè)一致性狀態(tài),無(wú)論事務(wù)是否執(zhí)行成功。
- 命令加入事務(wù)隊(duì)列失敗(參數(shù)個(gè)數(shù)不對(duì)?命令不存在?),整個(gè)事務(wù)不會(huì)執(zhí)行。所以事務(wù)的一致性不會(huì)被影響。
- 使用了watch命令監(jiān)視的key只事務(wù)期間被其他客戶端修改,整個(gè)事務(wù)不會(huì)執(zhí)行。也不會(huì)影響事務(wù)的一致性。
- 命令執(zhí)行錯(cuò)誤。如果事務(wù)執(zhí)行過(guò)程中有一個(gè)活多個(gè)命令錯(cuò)誤執(zhí)行失敗,服務(wù)器也不會(huì)中斷事務(wù)的執(zhí)行,會(huì)繼續(xù)執(zhí)行事務(wù)中剩下的命令,并且已經(jīng)執(zhí)行的命令不會(huì)受任何影響。出錯(cuò)的命令將不會(huì)執(zhí)行,也就不會(huì)對(duì)數(shù)據(jù)庫(kù)做出修改,因此這種情況下事物的一致性也不會(huì)受到影響。
- 服務(wù)器宕機(jī)。服務(wù)器宕機(jī)的情況下的一致性可以根據(jù)服務(wù)器使用的持久化方式來(lái)分析。
- 無(wú)持久化模式下,事務(wù)是一致的。這種情況下重啟之后的數(shù)據(jù)庫(kù)沒(méi)有任何數(shù)據(jù),因此總是一致的。
- RDB模式下,事務(wù)也是一致的。服務(wù)器宕機(jī)重啟之后可以根據(jù)RDB文件來(lái)恢復(fù)數(shù)據(jù),從而將數(shù)據(jù)庫(kù)還原到一個(gè)一致的狀態(tài)。如果找不到可以使用的RDB文件,那么重啟之后數(shù)據(jù)庫(kù)是空白的,那也是一致的。
- AOF模式下,事務(wù)也是一致的。服務(wù)器宕機(jī)重啟之后可以根據(jù)AOF文件來(lái)恢復(fù)數(shù)據(jù),從而將數(shù)據(jù)庫(kù)還原到一個(gè)一直的狀態(tài)。如果找不到可以使用的AOF文件,那么重啟之后數(shù)據(jù)庫(kù)是空白的,那么也是一致的。
Isolation:隔離性
Redis 是單進(jìn)程程序,并且它保證在執(zhí)行事務(wù)時(shí),不會(huì)對(duì)事務(wù)進(jìn)行中斷,事務(wù)可以運(yùn)行直到執(zhí)行完所有事務(wù)隊(duì)列中的命令為止。因此,Redis 的事務(wù)是總是帶有隔離性的。
Durability:持久性
Redis事務(wù)并沒(méi)有提供任何的持久性功能,所以事務(wù)的持久性是由Redis本身所使用的持久化方式來(lái)決定的。
- 在單純的內(nèi)存模式下,事務(wù)肯定是不持久的。
- 在RDB模式下,服務(wù)器可能在事務(wù)執(zhí)行之后RDB文件更新之前的這段時(shí)間失敗,所以RDB模式下的Redis事務(wù)也是不持久的。
- 在AOF的always模式下,事務(wù)的每條命令在執(zhí)行成功之后,都會(huì)立即調(diào)用fsync或fdatasync將事務(wù)數(shù)據(jù)寫入到AOF文件。但是,這種保存是由后臺(tái)線程進(jìn)行的,主線程不會(huì)阻塞直到保存成功,所以從命令執(zhí)行成功到數(shù)據(jù)保存到硬盤之間,還是有一段非常小的間隔,所以這種模式下的事務(wù)也是不持久的。
- 其他AOF模式也和always模式類似,所以它們都是不持久的。
結(jié)論:Redis的事務(wù)滿足原子性、一致性和隔離性,但是不滿足持久性。
Reference
- Redis源碼(3.2.28)
- 《Redis設(shè)計(jì)與實(shí)現(xiàn)》