Redis通過MULTI、EXEC、WATCH等命令來實(shí)現(xiàn)事務(wù)(transaction)功能。事務(wù)提供了一種將多個(gè)命令請(qǐng)求打包,然后一次性、按順序地執(zhí)行多個(gè)命令的機(jī)制,并且在事務(wù)執(zhí)行期間,服務(wù)器不會(huì)中斷事務(wù)而去執(zhí)行其他客戶端命令。
1 事務(wù)的實(shí)現(xiàn)
??一個(gè)事務(wù)從開始到結(jié)束通常經(jīng)歷三個(gè)階段:
(1) 事務(wù)開始
(2) 命令入隊(duì)
(3) 事務(wù)執(zhí)行
??1.1 事務(wù)開始
MULTI命令的執(zhí)行標(biāo)志著事務(wù)的開始:
127.0.0.1:6379> MULTIOK
MULTI命令可以將執(zhí)行該命令的客戶端從非事務(wù)狀態(tài)切換至事務(wù)狀態(tài)。
??1.2 命令入隊(duì)
??當(dāng)一個(gè)客戶端處于非事務(wù)狀態(tài)時(shí),這個(gè)客戶端的命令會(huì)立即被服務(wù)器執(zhí)行。與此不同的是,當(dāng)一個(gè)客戶端切換到事務(wù)狀態(tài)之后,服務(wù)器會(huì)根據(jù)這個(gè)客戶端發(fā)來的不同的命令執(zhí)行不同的操作:
(1) 如果客戶端發(fā)送的命令為EXEC、DISCARD、WATCH、MULTI四個(gè)命令中的其中一個(gè),那么服務(wù)器會(huì)立即執(zhí)行這個(gè)命令。
(2) 如果客戶端發(fā)送的命令是EXEC、DISCARD、WATCH、MULTI四個(gè)命令以為的其他命令,那么服務(wù)器不會(huì)立即執(zhí)行這個(gè)命令,而是將這個(gè)命令放在一個(gè)事務(wù)隊(duì)列中,然后向客戶端返回QUEUED回復(fù)。

服務(wù)器判斷命令是該入隊(duì)還是該執(zhí)行的過程
??1.3 事務(wù)隊(duì)列
??事務(wù)隊(duì)列是一個(gè)以先進(jìn)先出(FIFO)的方式保存入隊(duì)的命令,較先入隊(duì)的命令會(huì)被放到數(shù)組的前面,而較后入隊(duì)的命令則會(huì)被放到數(shù)組的后面。例如:
127.0.0.1:6379>> MULTIOK127.0.0.1:6379>> set name Redis;
127.0.0.1:6379>> get name;
127.0.0.1:6379>> set author"Peter Seibei";
127.0.0.1:6379>> get author;
那么服務(wù)器器將會(huì)為客戶端創(chuàng)建一個(gè)事務(wù)隊(duì)列,將上面4條命令入隊(duì),最先入隊(duì)的SET命令放在事務(wù)隊(duì)列的最前面……:

事務(wù)隊(duì)列
??1.4 執(zhí)行事務(wù)
當(dāng)一個(gè)處于事務(wù)狀態(tài)的客戶端向服務(wù)器發(fā)送EXEC命令時(shí),這個(gè)EXEC命令將立即被服務(wù)器執(zhí)行。服務(wù)器會(huì)遍歷這個(gè)客戶端的事務(wù)隊(duì)列,執(zhí)行隊(duì)列中保存的所有的命令,最后將執(zhí)行命令所得的結(jié)果返回給客戶端。對(duì)于上例子:
127.0.0.1:6379> EXEC1)
OK2)
"Redis"3)
OK4)
"Peter Seibei"
2 WATCH命令的實(shí)現(xiàn)
WATCH命令是一個(gè)樂觀所(optimistic locking),它可以在EXEC命令執(zhí)行前,監(jiān)視任意數(shù)量的數(shù)據(jù)庫鍵,并在EXEC命令執(zhí)行時(shí),檢查監(jiān)視的鍵是否至少有一個(gè)已經(jīng)被修改過了,如果修改過了,服務(wù)器將拒絕執(zhí)行事務(wù),并向客戶端返回代表事務(wù)執(zhí)行失敗的空回復(fù)。例如,下面的事務(wù)將會(huì)執(zhí)行失?。?/p>
127.0.0.1:6379>> WATCH
?nameOK
127.0.0.1:6379>> MULTI
OK
127.0.0.1:6379>> set"name""Peter"
127.0.0.1:6379>> EXEC(nil)
時(shí)間客戶端A客戶端B
T1WATCH "name"
T2MULTI
T3SET "name" "Peter"
T4SET "name" "jack"
T5EXEC
在時(shí)間T4,客戶端B修改了"name"鍵的值,當(dāng)客戶端A在T5執(zhí)行EXEC命令時(shí),服務(wù)器會(huì)發(fā)現(xiàn)WATCH監(jiān)視的鍵“name”已經(jīng)被修改,因此服務(wù)器拒絕執(zhí)行客戶端A的事務(wù),并向客戶端A返回空回復(fù)。
??2.1使用WATCH命令監(jiān)視數(shù)據(jù)庫鍵
每個(gè)Redis數(shù)據(jù)庫保存著一個(gè)watched_keys字典,這個(gè)字典的鍵是某個(gè)被WATCH命令監(jiān)視的數(shù)據(jù)庫鍵,而字典的值是一個(gè)鏈表,鏈表記錄了所有監(jiān)視相應(yīng)數(shù)據(jù)庫鍵的客戶端。

一個(gè)watched_keys字典
上圖表明:
(1) 客戶端c1和c2正在監(jiān)視鍵“name”。
(2) 客戶端c3正在監(jiān)視鍵“age”。
(3) 客戶端c2和c4正在監(jiān)視鍵“address”。
??2.2 監(jiān)視機(jī)制的觸發(fā)
所有對(duì)數(shù)據(jù)庫進(jìn)行修改命令,如SET、LPUSH、SADD、ZREM、DEL等,在執(zhí)行后都會(huì)對(duì)watched_keys字典進(jìn)行檢查,查看被修改的數(shù)據(jù)庫鍵是否是被客戶端所監(jiān)視的鍵,如果有的話,客戶端REDIS_DIRTY_CAS標(biāo)識(shí)將會(huì)被打開,表示該客戶端的事務(wù)安全性已經(jīng)被破壞。
??2.3 判斷事務(wù)是否安全
當(dāng)服務(wù)器接收到一個(gè)客戶端發(fā)來的EXEC命令,服務(wù)器會(huì)根據(jù)這個(gè)客戶端是否打開了REDIS_DIRTY_CAS標(biāo)識(shí)來決定是否執(zhí)行事務(wù):
(1) 如果客戶端的REDIS_DIRTY_ CAS標(biāo)識(shí)已經(jīng)被打開,那么說明客戶端所監(jiān)視的鍵當(dāng)中,至少有一個(gè)鍵已經(jīng)被修改過了,在這種情況下,客戶端提交的事務(wù)已經(jīng)不再安全,所以服務(wù)器拒絕執(zhí)行客戶端提交的事務(wù)。
(2) 如果客戶端的REDIS_DIRTY_CAS標(biāo)識(shí)沒有被打開,那么說明客戶端監(jiān)視的所有鍵都沒有被修改過(或者客戶端沒有監(jiān)視任何鍵),事務(wù)仍然是安全的,服務(wù)器將執(zhí)行客戶端提交的這個(gè)事務(wù)。
3 事務(wù)的ACID性質(zhì)
??在Redis中,事務(wù)總是具有原子性(Atomicity)、一致性(Consistency)和隔離性(Isolation),并且當(dāng)Redis運(yùn)行在某種特定的持久化模式下,事務(wù)也具有耐久性(Durability)。
??3.1 原子性
事務(wù)具有原子性指的是, 數(shù)據(jù)庫將事務(wù)中的多個(gè)操作當(dāng)作一個(gè)整體來執(zhí)行,服務(wù)器要么就執(zhí)行事務(wù)中的所有操作, 要么就一個(gè)操作也不執(zhí)行。
對(duì)于Redis的事務(wù)功能來說,事務(wù)隊(duì)列中的命令要么就全部都執(zhí)行,要么就一個(gè)都不執(zhí)行,因此, Redis的事務(wù)是具有原子性的。
下面是一個(gè)執(zhí)行失敗的事務(wù),這個(gè)事務(wù)因?yàn)槊钊腙?duì)出錯(cuò)而被服務(wù)器拒絕執(zhí)行:
127.0.0.1:6379>multi
OK
127.0.0.1:6379>set msg hello
127.0.0.1:6379>get(error)ERRwrong number of argumentsfor'get'command
127.0.0.1:6379>get msg
127.0.0.1:6379>exec
(error) EXEC ABORT Transactiondiscarded because of previous errors.
Redis的事務(wù)和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫事務(wù)的最大區(qū)別在于,Redis不支持事務(wù)回滾機(jī)制(rollback), 即使事務(wù)隊(duì)列中的某個(gè)命令在執(zhí)行期間出現(xiàn)了錯(cuò)誤,整個(gè)事務(wù)也會(huì)繼續(xù)執(zhí)行下去,直到將事務(wù)隊(duì)列中的所有命令都執(zhí)行完畢為止。
下面展示了即使RPUSH命令在執(zhí)行期間出現(xiàn)了錯(cuò)誤,事務(wù)的后續(xù)命令也會(huì)繼續(xù)執(zhí)行下去, 并且之前執(zhí)行的命令也不會(huì)有任何影響:
127.0.0.1:6379> set msg hello
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd fruit apple banana cherry
127.0.0.1:6379> rpush msg bye redis
127.0.0.1:6379> sadd alphabet a b c
127.0.0.1:6379> exec
1)(integer)3
2)(error)WRONGTYPE Operation against a key holding the wrong kind of value
3)(integer)3
??Redis為什么不支持回滾:不支持事務(wù)回滾是因?yàn)檫@種復(fù)雜的功能和Redis追求簡單高效的設(shè)計(jì)主旨不相符,并且Redis事務(wù)的執(zhí)行時(shí)錯(cuò)誤通常都是編程錯(cuò)誤產(chǎn)生的, 這種錯(cuò)誤通常只會(huì)出現(xiàn)在開發(fā)環(huán)境中, 而很少會(huì)在實(shí)際的生產(chǎn)環(huán)境中出現(xiàn)。
??3.2 一致性
??事務(wù)的一致性是指,如果數(shù)據(jù)庫執(zhí)行前是一致的,那么在事務(wù)執(zhí)行后,無論事務(wù)是否執(zhí)行成功,數(shù)據(jù)庫也應(yīng)該是一致的。
??3.2.1入隊(duì)錯(cuò)誤
如果一個(gè)事務(wù)在入隊(duì)命令的過程中,出現(xiàn)了命令不存在,或者命令的格式不正確等情況, 那么Redis將拒絕執(zhí)行這個(gè)事務(wù)。因?yàn)榉?wù)器會(huì)拒絕執(zhí)行人隊(duì)過程中出現(xiàn)錯(cuò)誤的事務(wù), 所以Redis事務(wù)的一致性不會(huì)被帶有入隊(duì)錯(cuò)誤的事務(wù)影響。
127.0.0.1:6379>multi
OK
127.0.0.1:6379>set msg?
hello
127.0.0.1:6379>YAHOOO
(error)ERRunknown command'YAHOOO'
127.0.0.1:6379>get msg
127.0.0.1:6379>exec
(error)EXECABORTTransactiondiscarded because of previous errors.
Redis 2.6.5以前的入隊(duì)錯(cuò)誤處理:Redis會(huì)忽略錯(cuò)誤的命令,而正確的命令如上面的SET和GET仍然會(huì)被執(zhí)行。
??3.2.2 執(zhí)行錯(cuò)誤
執(zhí)行錯(cuò)誤通常都是一些不能在入隊(duì)時(shí)被服務(wù)器發(fā)現(xiàn)的錯(cuò)誤, 這些錯(cuò)誤只會(huì)在命令實(shí)際執(zhí)行時(shí)被觸發(fā)。即使在事務(wù)的執(zhí)行過程中發(fā)生了錯(cuò)誤, 服務(wù)器也不會(huì)中斷事務(wù)的執(zhí)行, 它會(huì)繼續(xù)執(zhí)行事務(wù)中余下的其他命令, 并且已執(zhí)行的命令(包括執(zhí)行命令所產(chǎn)生的結(jié)果)不會(huì)被出錯(cuò)的命令影響。
因?yàn)樵谑聞?wù)執(zhí)行的過程中, 出錯(cuò)的命令會(huì)被服務(wù)器識(shí)別出來, 并進(jìn)行相應(yīng)的錯(cuò)誤處理, 所以這些出錯(cuò)命令不會(huì)對(duì)數(shù)據(jù)庫做任何修改, 也不會(huì)對(duì)事務(wù)的一致性產(chǎn)生任何影響。
127.0.0.1:6379> set msg hello
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd fruit apple banana cherryQUEUED127.0.0.1:6379> rpush msg bye redis
QUEUED
127.0.0.1:6379> sadd alphabet a b c
QUEUED
127.0.0.1:6379> exec
1)(integer)3
2)(error)WRONGTYPE Operation against a key holding the wrong kind of value
3)(integer)3
??3.2.3 服務(wù)器停機(jī)
(1) 如果服務(wù)器運(yùn)行在無持久化的內(nèi)存模式下,那么重啟之后的數(shù)據(jù)庫將是空白的, 因此數(shù)據(jù)總是一致的。
(2) 如果服務(wù)器運(yùn)行在RDB模式或AOF模式下, 那么在事務(wù)中途停機(jī)不會(huì)導(dǎo)致不一致,因?yàn)榉?wù)器可以根據(jù)RDB文件或AOF文件來回復(fù)數(shù)據(jù),從而將數(shù)據(jù)庫還原到一個(gè)一致的狀態(tài)。
??3.3 隔離性
事務(wù)的隔離性指的是,即使數(shù)據(jù)庫中有多個(gè)事務(wù)并發(fā)地執(zhí)行,各個(gè)事務(wù)之間也不會(huì)互相 影響,并且在并發(fā)狀態(tài)下執(zhí)行的事務(wù)和串行執(zhí)行的事務(wù)產(chǎn)生的結(jié)果完全相同。
因?yàn)镽edis使用單線程的方式來執(zhí)行事務(wù)(以及事務(wù)隊(duì)列中的命令),并且服務(wù)器保證, 在執(zhí)行事務(wù)期間不會(huì)對(duì)事務(wù)進(jìn)行中斷,因此,Redis的事務(wù)總是以串行的方式運(yùn)行的,并且 事務(wù)也總是具有隔離性的。
??3.4 耐久性
事務(wù)的耐久性指的是,當(dāng)一個(gè)事務(wù)執(zhí)行完畢時(shí),執(zhí)行這個(gè)事務(wù)所得的結(jié)果巳經(jīng)被保存到 永久性存儲(chǔ)介質(zhì)(比如硬盤)里面了, 即使服務(wù)器在事務(wù)執(zhí)行完畢 之后停機(jī), 執(zhí)行事務(wù)所得的結(jié)果也不會(huì)丟失。Redis事務(wù)的耐久性由服務(wù)器所使用持久化模式?jīng)Q定的:
(1) 當(dāng)服務(wù)器在無持久化的內(nèi)存模式下運(yùn)作時(shí),事務(wù)不具有耐久性。因?yàn)橐坏┓?wù)器停機(jī),
服務(wù)器所有的數(shù)據(jù)都將丟失。
(2) 當(dāng)服務(wù)器在ROB持久化模式下運(yùn)作時(shí),事務(wù)同樣不具有耐久性。因?yàn)榉?wù)器只會(huì)在特定的保存條件下才會(huì)執(zhí)行BGSAVE命令,并且異步執(zhí)行的BGSAVE命令不能保證事務(wù)的數(shù)據(jù)第一時(shí)間被保存到硬盤上。
(3) 當(dāng)服務(wù)器運(yùn)行在AOF持久化模式下,并且appendfsync選項(xiàng)的值為always時(shí),程序總會(huì)在執(zhí)行命令之后調(diào)用同步(sync)函數(shù),將命令數(shù)據(jù)真正地保存到硬盤里。
4 小結(jié)
(1) 事務(wù)提供了一種將多個(gè)命令打包,然后一次性、有序地執(zhí)行的機(jī)制。
(2) 多個(gè)命令會(huì)被人隊(duì)到事務(wù)隊(duì)列中, 然后按先進(jìn)先出(FIFO)的順序執(zhí)行。
(3) 事務(wù)在執(zhí)行過程中不會(huì)被中斷,當(dāng)事務(wù)隊(duì)列中的所有命令都被執(zhí)行完畢之后,事務(wù)
才會(huì)結(jié)束。
(4) 帶有WATCH命令的事務(wù)會(huì)將客戶端和被監(jiān)視的鍵在數(shù)據(jù)庫的watched_keys字典關(guān)聯(lián),當(dāng)鍵被修改時(shí),程序會(huì)將所有監(jiān)視被修改鍵的客戶端的REDIS_DIRTY_CAS標(biāo)識(shí)打開,服務(wù)只有在REDIS_DIRTY_CAS標(biāo)識(shí)沒有打開時(shí),才會(huì)執(zhí)行客戶端提交的事務(wù),否則服務(wù)器拒絕執(zhí)行事務(wù)。
(5) Redis事務(wù)不支持回滾機(jī)制。
(6) Redis的事務(wù)總是具有ACID中的原子性、一致性和隔離性,當(dāng)服務(wù)器運(yùn)行在AOF持久化模式下,并且appendfsync選項(xiàng)的值為always時(shí),事務(wù)也具有耐久性。