1.從數(shù)據(jù)庫事務(wù)說起
通常我們提及數(shù)據(jù)庫都不可避免的要提到事務(wù),那么什么是事務(wù)呢?事務(wù)是指作為單個邏輯工作單元執(zhí)行的一系列操作。所以,首先事務(wù)是一系列操作,這一系列操作具有二態(tài)性,即完全地執(zhí)行或者完全地不執(zhí)行。因此事務(wù)處理可以確保除非事務(wù)單元內(nèi)的所有操作的成功完成,否則不會想數(shù)據(jù)庫更新面向數(shù)據(jù)的資源。我們這里舉一個例子,數(shù)據(jù)庫中除查詢操作以外,插入(Insert)、刪除(Delete)和更新(Update)這三種操作都會對數(shù)據(jù)造成影響,因?yàn)槭聞?wù)處理能夠保證一系列操作可以完全地執(zhí)行或者完全不執(zhí)行,因此在一個事務(wù)被提交以后,該事務(wù)中的任何一條SQL語句在被執(zhí)行的時候,都會生成一條撤銷日志(Undo Log),而撤銷日志中記錄的是和當(dāng)前擦作完全相反的操作,比如刪除的相反操作是插入,插入的相反操作是刪除等。我們通常所說的事務(wù)回滾其實(shí)就是去執(zhí)行這些插銷日志里的相反操作,這同樣告訴我們一個道理,只有事務(wù)中的一系列操作完全執(zhí)行的情況下可以回滾,如果是在意外情況下導(dǎo)致事務(wù)中的一系列操作沒有完全執(zhí)行,這個時候我們是不能保證數(shù)據(jù)一定可以回滾的。
在數(shù)據(jù)庫相關(guān)理論中,一個邏輯工作單元想要成為事務(wù),就必須滿足ACID,即原子性、一致性、隔離性和持久性。
- (1)原子性:原子性這個概念其實(shí)就是指,一個事務(wù)內(nèi)的所有SQL操作都是一個整體,因此只有所有的SQL操作都完全執(zhí)行成功,該事務(wù)方可以認(rèn)為提交成功。如果在提交事務(wù)過程中某一條SQL語句執(zhí)行失敗,則整個事務(wù)必須回滾到事務(wù)提交前的狀態(tài)。
- (2)一致性:而一致性這個概念則是指,事務(wù)在完成的時候,必須要保證所有的數(shù)據(jù)都保持一致的狀態(tài),而落實(shí)到數(shù)據(jù)庫的各個組成部分上,則要求開發(fā)人員能夠保證數(shù)據(jù)、索引、約束、日志等在事務(wù)前后具備一致性。
- (3)隔離性:隔離性這個概念主要針對并發(fā),其核心思想就是不同的并發(fā)事務(wù)對數(shù)據(jù)產(chǎn)生的修改必須是相互隔離的,假設(shè)有兩個不同的事務(wù)A和B并發(fā)執(zhí)行,那么對A來講,它在執(zhí)行前的狀態(tài)只有兩種,即B執(zhí)行前和B執(zhí)行后。同理,對B來講同樣是如此,這樣的特性我們就稱為隔離性。
- (4)持久性:持久性相對簡單,是指事務(wù)完成以后它對數(shù)據(jù)的影響是永久性的。
2.Redis中的事務(wù)處理
我們對數(shù)據(jù)庫中事務(wù)處理的相關(guān)理論有了一個基本的認(rèn)識,或許這個世界上的數(shù)據(jù)庫系統(tǒng)千差萬別,但我相信在事務(wù)處理這個問題上它們最終會殊途同歸,就像我們解決并發(fā)過程中的沖突問題,常規(guī)的做法依然是加鎖一樣,這是我之所以要花費(fèi)精力去理解和解釋這些理論知識的原因,技術(shù)可謂是日新月異,如果我們總是一味地為新技術(shù)而疲于奔命,那么或許我們會漸漸地失去對這個行業(yè)的熱愛,我相信原理永遠(yuǎn)比框架更為重要。
redis事務(wù)提供了一種“將多個命令打包, 然后一次性、按順序地執(zhí)行”的機(jī)制, 并且事務(wù)在執(zhí)行的期間不會主動中斷 —— 服務(wù)器在執(zhí)行完事務(wù)中的所有命令之后, 才會繼續(xù)處理其他客戶端的其他命令。
Redis中的事務(wù)是可以視為一個隊(duì)列,即我們可以通過MULTI開始一個事務(wù),這相當(dāng)于我們聲明了一個命令隊(duì)列。接下來,我們向Redis中提交的每條命令,都會被排入這個命令隊(duì)列。當(dāng)我們輸入EXEC命令時,將觸發(fā)當(dāng)前事務(wù),這相當(dāng)于我們從命令隊(duì)列中取出命令并執(zhí)行,所以Redis中一個事務(wù)從開始到執(zhí)行會經(jīng)歷 開始事務(wù) 、 命令入隊(duì) 和 執(zhí)行事務(wù) 三個階段。下面是一個在Redis中使用事務(wù)的簡單示例:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET Book_Name "GIt Pro"
QUEUED
127.0.0.1:6379> SADD Program_Language "C++" "C#" "Jave" "Python"
QUEUED
127.0.0.1:6379> GET Book_Name
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 4
3) "GIt Pro"
我們可以注意到Redis中的事務(wù)和通常意義上的事務(wù)基本上是一致的,即
- 事務(wù)是由一系列操作組成的單個邏輯工作執(zhí)行單元。特別地,因?yàn)樵赗edis中命令是存儲在一個隊(duì)列中,所以,事務(wù)中的所有命令都會按順序執(zhí)行,并且在執(zhí)行事務(wù)的過程中不會被客戶端發(fā)送的其它命令中斷。
- 事務(wù)是一個原子操作,事物中的命令只有兩種執(zhí)行結(jié)果,即全部執(zhí)行或者全部不執(zhí)行。如果客戶端在使用MULTI命令開啟事務(wù)后因?yàn)橐馔舛鴽]有執(zhí)行EXEC命令,則事務(wù)中的所有命令都不會執(zhí)行。同理,如果客戶端在使用MULTI命令開啟事務(wù)后執(zhí)行EXEC命令,則事務(wù)中的所有命令都會執(zhí)行。
- Redis中的事務(wù)可以使用DISCARD命令來清空一個命令隊(duì)列,并放棄對事務(wù)的執(zhí)行。如果命令在入隊(duì)時發(fā)生錯誤,Redis將在客戶端調(diào)用EXEC命令時拒絕執(zhí)行并取消事務(wù),但是在EXEC命令執(zhí)行后發(fā)生的錯誤,Redis將選擇自動忽略。
3.redis事務(wù)執(zhí)行過程
一個事務(wù)從開始到執(zhí)行會經(jīng)歷以下三個階段:
- 1)開始事務(wù)。
- 2)命令入隊(duì)。
- 3)執(zhí)行事務(wù)。
下面將分別介紹事務(wù)的這三個階段。
1)開始事務(wù)
MULTI命令的執(zhí)行標(biāo)記著事務(wù)的開始:
redis> MULTI
OK
這個命令唯一做的就是, 將客戶端的 REDIS_MULTI 選項(xiàng)打開, 讓客戶端從非事務(wù)狀態(tài)切換到事務(wù)狀態(tài)。

2)命令入隊(duì)
當(dāng)客戶端處于非事務(wù)狀態(tài)下時, 所有發(fā)送給服務(wù)器端的命令都會立即被服務(wù)器執(zhí)行:
redis> SET msg "hello moto"
OK
redis> GET msg
"hello moto"
但是, 當(dāng)客戶端進(jìn)入事務(wù)狀態(tài)之后, 服務(wù)器在收到來自客戶端的命令時, 不會立即執(zhí)行命令, 而是將這些命令全部放進(jìn)一個事務(wù)隊(duì)列里, 然后返回QUEUED, 表示命令已入隊(duì):
redis> MULTI
OK
redis> SET msg "hello moto"
QUEUED
redis> GET msg
QUEUED
其原理如圖2所示

3)執(zhí)行事務(wù)
前面說到, 當(dāng)客戶端進(jìn)入事務(wù)狀態(tài)之后, 客戶端發(fā)送的命令就會被放進(jìn)事務(wù)隊(duì)列里。
但其實(shí)并不是所有的命令都會被放進(jìn)事務(wù)隊(duì)列, 其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 這四個命令 —— 當(dāng)這四個命令從客戶端發(fā)送到服務(wù)器時, 它們會像客戶端處于非事務(wù)狀態(tài)一樣, 直接被服務(wù)器執(zhí)行:

如果客戶端正處于事務(wù)狀態(tài), 那么當(dāng)EXEC命令執(zhí)行時, 服務(wù)器根據(jù)客戶端所保存的事務(wù)隊(duì)列, 以先進(jìn)先出(FIFO)的方式執(zhí)行事務(wù)隊(duì)列中的命令: 最先入隊(duì)的命令最先執(zhí)行, 而最后入隊(duì)的命令最后執(zhí)行。
執(zhí)行事務(wù)中的命令所得的結(jié)果會以 FIFO 的順序保存到一個回復(fù)隊(duì)列中。
當(dāng)事務(wù)隊(duì)列里的所有命令被執(zhí)行完之后,EXEC命令會將回復(fù)隊(duì)列作為自己的執(zhí)行結(jié)果返回給客戶端, 客戶端從事務(wù)狀態(tài)返回到非事務(wù)狀態(tài), 至此, 事務(wù)執(zhí)行完畢。
4.redis事務(wù)命令
redis事務(wù)使用了multi、exec、discard、watch、unwatch命令,命令的作用如圖4所示:

使用案例:
- 正常執(zhí)行

- 放棄事務(wù)

- 若在事務(wù)隊(duì)列中存在命令性錯誤,則執(zhí)行EXEC命令時,所有命令都不會執(zhí)行

- 若在事務(wù)隊(duì)列中存在語法性錯誤,則執(zhí)行EXEC命令時,其他正確命令會被執(zhí)行,錯誤命令拋出異常。

- 使用watch
使用watch檢測balance,事務(wù)期間balance數(shù)據(jù)未變動,事務(wù)執(zhí)行成功

WATCH命令用于在事務(wù)開始之前監(jiān)視任意數(shù)量的鍵: 當(dāng)調(diào)用EXEC命令執(zhí)行事務(wù)時, 如果任意一個被監(jiān)視的鍵已經(jīng)被其他客戶端修改了, 那么整個事務(wù)不再執(zhí)行, 直接返回失敗。


- WATCH 命令的實(shí)現(xiàn)
在每個代表數(shù)據(jù)庫的 redis.h/redisDb 結(jié)構(gòu)類型中, 都保存了一個 watched_keys 字典, 字典的鍵是這個數(shù)據(jù)庫被監(jiān)視的鍵, 而字典的值則是一個鏈表, 鏈表中保存了所有監(jiān)視這個鍵的客戶端。
比如說,以下字典就展示了一個 watched_keys 字典的例子:

其中, 鍵 key1 正在被 client2 、 client5 和 client1 三個客戶端監(jiān)視, 其他一些鍵也分別被其他別的客戶端監(jiān)視著。
WATCH 命令的作用, 就是將當(dāng)前客戶端和要監(jiān)視的鍵在 watched_keys 中進(jìn)行關(guān)聯(lián)。
舉個例子, 如果當(dāng)前客戶端為 client10086 , 那么當(dāng)客戶端執(zhí)行 WATCH key1 key2 時, 前面展示的 watched_keys 將被修改成這個樣子:

通過watched_keys字典, 如果程序想檢查某個鍵是否被監(jiān)視, 那么它只要檢查字典中是否存在這個鍵即可; 如果程序要獲取監(jiān)視某個鍵的所有客戶端, 那么只要取出鍵的值(一個鏈表), 然后對鏈表進(jìn)行遍歷即可。
-
watch的觸發(fā)
在任何對數(shù)據(jù)庫鍵空間(key space)進(jìn)行修改的命令成功執(zhí)行之后 (比如FLUSHDB、SET、DEL、LPUSH、SADD、ZREM,諸如此類),multi.c/touchWatchedKey函數(shù)都會被調(diào)用 —— 它檢查數(shù)據(jù)庫的watched_keys字典, 看是否有客戶端在監(jiān)視已經(jīng)被命令修改的鍵, 如果有的話, 程序?qū)⑺斜O(jiān)視這個/這些被修改鍵的客戶端的REDIS_DIRTY_CAS選項(xiàng)打開:

當(dāng)客戶端發(fā)送 EXEC 命令、觸發(fā)事務(wù)執(zhí)行時, 服務(wù)器會對客戶端的狀態(tài)進(jìn)行檢查:
- 如果客戶端的
REDIS_DIRTY_CAS選項(xiàng)已經(jīng)被打開,那么說明被客戶端監(jiān)視的鍵至少有一個已經(jīng)被修改了,事務(wù)的安全性已經(jīng)被破壞。服務(wù)器會放棄執(zhí)行這個事務(wù),直接向客戶端返回空回復(fù),表示事務(wù)執(zhí)行失敗。 - 如果
REDIS_DIRTY_CAS選項(xiàng)沒有被打開,那么說明所有監(jiān)視鍵都安全,服務(wù)器正式執(zhí)行事務(wù)。
5. 事務(wù)的 ACID 性質(zhì)
在Redis中,事務(wù)總是具有原子性(Atomicity)、一致性(Consistency)和隔離性(Isolation),并且當(dāng)Redis運(yùn)行在某種特定的持久化模式下,事務(wù)也具有持久性性(Durability)。
- 原子性
事務(wù)具有原子性指的是, 數(shù)據(jù)庫將事務(wù)中的多個操作當(dāng)作一個整體來執(zhí)行,服務(wù)器要么就執(zhí)行事務(wù)中的所有操作, 要么就一個操作也不執(zhí)行。
對于Redis的事務(wù)功能來說,事務(wù)隊(duì)列中的命令要么就全部都執(zhí)行,要么就一個都不執(zhí)行,因此, Redis的事務(wù)是具有原子性的。
Redis的事務(wù)和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫事務(wù)的最大區(qū)別在于,Redis不支持事務(wù)回滾機(jī)制(rollback), 即使事務(wù)隊(duì)列中的某個命令在執(zhí)行期間出現(xiàn)了錯誤,整個事務(wù)也會繼續(xù)執(zhí)行下去,直到將事務(wù)隊(duì)列中的所有命令都執(zhí)行完畢為止。 下面展示了即使RPUSH命令在執(zhí)行期間出現(xiàn)了錯誤,事務(wù)的后續(xù)命令也會繼續(xù)執(zhí)行下去, 并且之前執(zhí)行的命令也不會有任何影響:
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
QUEUED
127.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
不支持事務(wù)回滾是因?yàn)檫@種復(fù)雜的功能和Redis追求簡單高效的設(shè)計(jì)主旨不相符,并且Redis事務(wù)的執(zhí)行時錯誤通常都是編程錯誤產(chǎn)生的, 這種錯誤通常只會出現(xiàn)在開發(fā)環(huán)境中, 而很少會在實(shí)際的生產(chǎn)環(huán)境中出現(xiàn)。
- 一致性
事務(wù)的一致性是指,如果數(shù)據(jù)庫執(zhí)行前是一致的,那么在事務(wù)執(zhí)行后,無論事務(wù)是否執(zhí)行成功,數(shù)據(jù)庫也應(yīng)該是一致的。
隔離性
事務(wù)的隔離性指的是,即使數(shù)據(jù)庫中有多個事務(wù)并發(fā)地執(zhí)行,各個事務(wù)之間也不會互相 影響,并且在并發(fā)狀態(tài)下執(zhí)行的事務(wù)和串行執(zhí)行的事務(wù)產(chǎn)生的結(jié)果完全相同。
因?yàn)镽edis使用單線程的方式來執(zhí)行事務(wù)(以及事務(wù)隊(duì)列中的命令),并且服務(wù)器保證, 在執(zhí)行事務(wù)期間不會對事務(wù)進(jìn)行中斷,因此,Redis的事務(wù)總是以串行的方式運(yùn)行的,并且 事務(wù)也總是具有隔離性的。持久性
事務(wù)的耐久性指的是,當(dāng)一個事務(wù)執(zhí)行完畢時,執(zhí)行這個事務(wù)所得的結(jié)果巳經(jīng)被保存到 永久性存儲介質(zhì)(比如硬盤)里面了, 即使服務(wù)器在事務(wù)執(zhí)行完畢 之后停機(jī), 執(zhí)行事務(wù)所得的結(jié)果也不會丟失。Redis事務(wù)的耐久性由服務(wù)器所使用持久化模式?jīng)Q定的:
(1) 當(dāng)服務(wù)器在無持久化的內(nèi)存模式下運(yùn)作時,事務(wù)不具有耐久性。因?yàn)橐坏┓?wù)器停機(jī),
服務(wù)器所有的數(shù)據(jù)都將丟失。
(2) 當(dāng)服務(wù)器在ROB持久化模式下運(yùn)作時,事務(wù)同樣不具有耐久性。因?yàn)榉?wù)器只會在特定的保存條件下才會執(zhí)行BGSAVE命令,并且異步執(zhí)行的BGSAVE命令不能保證事務(wù)的數(shù)據(jù)第一時間被保存到硬盤上。
(3) 當(dāng)服務(wù)器運(yùn)行在AOF持久化模式下,并且appendfsync選項(xiàng)的值為always時,程序總會在執(zhí)行命令之后調(diào)用同步(sync)函數(shù),將命令數(shù)據(jù)真正地保存到硬盤里。