redis事務
Redis 通過 MULTI 舟奠、 DISCARD 泣栈、 EXEC 和 WATCH 四個命令來實現(xiàn)事務功能骇塘, 本章首先討論使用 MULTI 汛闸、 DISCARD 和 EXEC 三個命令實現(xiàn)的一般事務, 然后再來討論帶有 WATCH 的事務的實現(xiàn)平痰。
因為事務的安全性也非常重要汞舱, 所以本章最后通過常見的 ACID 性質(zhì)對 Redis 事務的安全性進行了說明。
事務
事務提供了一種“將多個命令打包宗雇, 然后一次性昂芜、按順序地執(zhí)行”的機制, 并且事務在執(zhí)行的期間不會主動中斷 —— 服務器在執(zhí)行完事務中的所有命令之后赔蒲, 才會繼續(xù)處理其他客戶端的其他命令泌神。
以下是一個事務的例子, 它先以 MULTI 開始一個事務舞虱, 然后將多個命令入隊到事務中欢际, 最后由 EXEC 命令觸發(fā)事務, 一并執(zhí)行事務中的所有命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"
一個事務從開始到執(zhí)行會經(jīng)歷以下三個階段:
- 開始事務矾兜。
- 命令入隊损趋。
- 執(zhí)行事務。
下文將分別介紹事務的這三個階段椅寺。
開始事務
MULTI 命令的執(zhí)行標記著事務的開始:
redis> MULTI
OK
這個命令唯一做的就是浑槽, 將客戶端的 REDIS_MULTI 選項打開, 讓客戶端從非事務狀態(tài)切換到事務狀態(tài)返帕。
命令入隊
當客戶端處于非事務狀態(tài)下時桐玻, 所有發(fā)送給服務器端的命令都會立即被服務器執(zhí)行:
redis> SET msg "hello moto"
OK
redis> GET msg
"hello moto"
但是, 當客戶端進入事務狀態(tài)之后荆萤, 服務器在收到來自客戶端的命令時镊靴, 不會立即執(zhí)行命令, 而是將這些命令全部放進一個事務隊列里, 然后返回 QUEUED 偏竟, 表示命令已入隊:
redis> MULTI
OK
redis> SET msg "hello moto"
QUEUED
redis> GET msg
QUEUED
以下流程圖展示了這一行為:
事務隊列是一個數(shù)組算行, 每個數(shù)組項是都包含三個屬性:
- 要執(zhí)行的命令(cmd)。
- 命令的參數(shù)(argv)苫耸。
- 參數(shù)的個數(shù)(argc)。
舉個例子儡陨, 如果客戶端執(zhí)行以下命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
那么程序?qū)榭蛻舳藙?chuàng)建以下事務隊列:
數(shù)組索引 | cmd | argv | argc |
---|---|---|---|
0 | SET | ["book-name", "Mastering C++ in 21 days"] | 2 |
1 | GET | ["book-name"] | 1 |
2 | SADD | ["tag", "C++", "Programming", "Mastering Series"] | 4 |
3 | SMEMBERS | ["tag"] | 1 |
執(zhí)行事務
前面說到褪子, 當客戶端進入事務狀態(tài)之后, 客戶端發(fā)送的命令就會被放進事務隊列里骗村。
但其實并不是所有的命令都會被放進事務隊列嫌褪, 其中的例外就是 EXEC 、 DISCARD 胚股、 MULTI 和 WATCH 這四個命令 —— 當這四個命令從客戶端發(fā)送到服務器時笼痛, 它們會像客戶端處于非事務狀態(tài)一樣, 直接被服務器執(zhí)行:
如果客戶端正處于事務狀態(tài)琅拌, 那么當 EXEC 命令執(zhí)行時缨伊, 服務器根據(jù)客戶端所保存的事務隊列, 以先進先出(FIFO)的方式執(zhí)行事務隊列中的命令: 最先入隊的命令最先執(zhí)行进宝, 而最后入隊的命令最后執(zhí)行刻坊。
比如說,對于以下事務隊列:
數(shù)組索引 | cmd | argv | argc |
---|---|---|---|
0 | SET | ["book-name", "Mastering C++ in 21 days"] | 2 |
1 | GET | ["book-name"] | 1 |
2 | SADD | ["tag", "C++", "Programming", "Mastering Series"] | 4 |
3 | SMEMBERS | ["tag"] | 1 |
程序會首先執(zhí)行 SET
命令党晋, 然后執(zhí)行 GET
命令谭胚, 再然后執(zhí)行 SADD
命令, 最后執(zhí)行 SMEMBERS
命令未玻。
執(zhí)行事務中的命令所得的結(jié)果會以 FIFO 的順序保存到一個回復隊列中灾而。
比如說,對于上面給出的事務隊列扳剿,程序?qū)殛犃兄械拿顒?chuàng)建如下回復隊列:
數(shù)組索引 | 回復類型 | 回復內(nèi)容 |
---|---|---|
0 | status code reply | OK |
1 | bulk reply | "Mastering C++ in 21 days" |
2 | integer reply | 3 |
3 | multi-bulk reply | ["Mastering Series", "C++", "Programming"] |
當事務隊列里的所有命令被執(zhí)行完之后旁趟, EXEC 命令會將回復隊列作為自己的執(zhí)行結(jié)果返回給客戶端, 客戶端從事務狀態(tài)返回到非事務狀態(tài)舞终, 至此轻庆, 事務執(zhí)行完畢。
事務的整個執(zhí)行過程可以用以下偽代碼表示:
def execute_transaction():
# 創(chuàng)建空白的回復隊列
reply_queue = []
# 取出事務隊列里的所有命令敛劝、參數(shù)和參數(shù)數(shù)量
for cmd, argv, argc in client.transaction_queue:
# 執(zhí)行命令余爆,并取得命令的返回值
reply = execute_redis_command(cmd, argv, argc)
# 將返回值追加到回復隊列末尾
reply_queue.append(reply)
# 清除客戶端的事務狀態(tài)
clear_transaction_state(client)
# 清空事務隊列
clear_transaction_queue(client)
# 將事務的執(zhí)行結(jié)果返回給客戶端
send_reply_to_client(client, reply_queue)
在事務和非事務狀態(tài)下執(zhí)行命令
無論在事務狀態(tài)下, 還是在非事務狀態(tài)下夸盟, Redis 命令都由同一個函數(shù)執(zhí)行蛾方, 所以它們共享很多服務器的一般設(shè)置, 比如 AOF 的配置、RDB 的配置桩砰,以及內(nèi)存限制拓春,等等。
不過事務中的命令和普通命令在執(zhí)行上還是有一點區(qū)別的亚隅,其中最重要的兩點是:
-
非事務狀態(tài)下的命令以單個命令為單位執(zhí)行硼莽,前一個命令和后一個命令的客戶端不一定是同一個;
而事務狀態(tài)則是以一個事務為單位煮纵,執(zhí)行事務隊列中的所有命令:除非當前事務執(zhí)行完畢懂鸵,否則服務器不會中斷事務,也不會執(zhí)行其他客戶端的其他命令行疏。
-
在非事務狀態(tài)下匆光,執(zhí)行命令所得的結(jié)果會立即被返回給客戶端;
而事務則是將所有命令的結(jié)果集合到回復隊列酿联,再作為 EXEC 命令的結(jié)果返回給客戶端终息。
事務狀態(tài)下的 DISCARD 、 MULTI 和 WATCH 命令
除了 EXEC
之外贞让, 服務器在客戶端處于事務狀態(tài)時周崭, 不加入到事務隊列而直接執(zhí)行的另外三個命令是 DISCARD 、 MULTI 和 WATCH 喳张。
DISCARD
命令用于取消一個事務休傍, 它清空客戶端的整個事務隊列, 然后將客戶端從事務狀態(tài)調(diào)整回非事務狀態(tài)蹲姐, 最后返回字符串 OK 給客戶端磨取, 說明事務已被取消。
Redis
的事務是不可嵌套的柴墩, 當客戶端已經(jīng)處于事務狀態(tài)忙厌, 而客戶端又再向服務器發(fā)送 MULTI 時, 服務器只是簡單地向客戶端發(fā)送一個錯誤江咳, 然后繼續(xù)等待其他命令的入隊逢净。 MULTI 命令的發(fā)送不會造成整個事務失敗, 也不會修改事務隊列中已有的數(shù)據(jù)歼指。
WATCH
只能在客戶端進入事務狀態(tài)之前執(zhí)行爹土, 在事務狀態(tài)下發(fā)送 WATCH 命令會引發(fā)一個錯誤, 但它不會造成整個事務失敗踩身, 也不會修改事務隊列中已有的數(shù)據(jù)(和前面處理 MULTI 的情況一樣)胀茵。
帶 WATCH 的事務
WATCH 命令用于在事務開始之前監(jiān)視任意數(shù)量的鍵: 當調(diào)用 EXEC 命令執(zhí)行事務時, 如果任意一個被監(jiān)視的鍵已經(jīng)被其他客戶端修改了挟阻, 那么整個事務不再執(zhí)行琼娘, 直接返回失敗峭弟。
以下示例展示了一個執(zhí)行失敗的事務例子:
redis> WATCH name
OK
redis> MULTI
OK
redis> SET name peter
QUEUED
redis> EXEC
(nil)
以下執(zhí)行序列展示了上面的例子是如何失敗的:
時間 | 客戶端 A | 客戶端 B |
---|---|---|
T1 | WATCH name | |
T2 | MULTI | |
T3 | SET name peter | |
T4 | SET name john | |
T5 | EXEC |
在時間 T4 ,客戶端 B 修改了 name 鍵的值脱拼, 當客戶端 A 在 T5 執(zhí)行 EXEC 時瞒瘸,Redis 會發(fā)現(xiàn) name 這個被監(jiān)視的鍵已經(jīng)被修改, 因此客戶端 A 的事務不會被執(zhí)行熄浓,而是直接返回失敗情臭。
下文就來介紹 WATCH 的實現(xiàn)機制,并且看看事務系統(tǒng)是如何檢查某個被監(jiān)視的鍵是否被修改赌蔑,從而保證事務的安全性的谎柄。
WATCH 命令的實現(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 命令的作用往史, 就是將當前客戶端和要監(jiān)視的鍵在 watched_keys 中進行關(guān)聯(lián)。
舉個例子佛舱, 如果當前客戶端為 client10086 椎例, 那么當客戶端執(zhí)行 WATCH key1 key2 時, 前面展示的 watched_keys 將被修改成這個樣子:
通過 watched_keys 字典请祖, 如果程序想檢查某個鍵是否被監(jiān)視订歪, 那么它只要檢查字典中是否存在這個鍵即可; 如果程序要獲取監(jiān)視某個鍵的所有客戶端肆捕, 那么只要取出鍵的值(一個鏈表)刷晋, 然后對鏈表進行遍歷即可。
WATCH 的觸發(fā)
在任何對數(shù)據(jù)庫鍵空間(key space)進行修改的命令成功執(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 選項打開:
當客戶端發(fā)送 EXEC 命令抒和、觸發(fā)事務執(zhí)行時矫渔, 服務器會對客戶端的狀態(tài)進行檢查:
如果客戶端的 REDIS_DIRTY_CAS 選項已經(jīng)被打開,那么說明被客戶端監(jiān)視的鍵至少有一個已經(jīng)被修改了摧莽,事務的安全性已經(jīng)被破壞庙洼。服務器會放棄執(zhí)行這個事務,直接向客戶端返回空回復镊辕,表示事務執(zhí)行失敗油够。
如果 REDIS_DIRTY_CAS 選項沒有被打開,那么說明所有監(jiān)視鍵都安全征懈,服務器正式執(zhí)行事務石咬。
可以用一段偽代碼來表示這個檢查:
def check_safety_before_execute_trasaction():
if client.state & REDIS_DIRTY_CAS:
# 安全性已破壞,清除事務狀態(tài)
clear_transaction_state(client)
# 清空事務隊列
clear_transaction_queue(client)
# 返回空回復給客戶端
send_empty_reply(client)
else:
# 安全性完好卖哎,執(zhí)行事務
execute_transaction()
舉個例子鬼悠,假設(shè)數(shù)據(jù)庫的 watched_keys 字典如下圖所示:
如果某個客戶端對 key1 進行了修改(比如執(zhí)行 DEL key1 )质帅, 那么所有監(jiān)視 key1 的客戶端阴挣, 包括 client2 嗽桩、 client5 和 client1 的 REDIS_DIRTY_CAS 選項都會被打開待牵, 當客戶端 client2 瓣俯、 client5 和 client1 執(zhí)行 EXEC 的時候沐序, 它們的事務都會以失敗告終棺弊。
最后帆啃,當一個客戶端結(jié)束它的事務時溯泣,無論事務是成功執(zhí)行虐秋,還是失敗, watched_keys 字典中和這個客戶端相關(guān)的資料都會被清除垃沦。
事務的 ACID 性質(zhì)
勘誤:Redis 的事務是保證原子性的熟妓,本節(jié)的內(nèi)容將原子性和回滾功能混淆了,等待修復中栏尚。 —— 2013.6.23
在傳統(tǒng)的關(guān)系式數(shù)據(jù)庫中起愈,常常用 ACID 性質(zhì)來檢驗事務功能的安全性。
Redis 事務保證了其中的一致性(C)和隔離性(I)译仗,但并不保證原子性(A)和持久性(D)抬虽。
以下四小節(jié)是關(guān)于這四個性質(zhì)的詳細討論。
原子性(Atomicity)
單個 Redis 命令的執(zhí)行是原子性的纵菌,但 Redis 沒有在事務上增加任何維持原子性的機制阐污,所以 Redis 事務的執(zhí)行并不是原子性的。
如果一個事務隊列中的所有命令都被成功地執(zhí)行咱圆,那么稱這個事務執(zhí)行成功笛辟。
另一方面功氨,如果 Redis 服務器進程在執(zhí)行事務的過程中被停止 —— 比如接到 KILL 信號、宿主機器停機手幢,等等捷凄,那么事務執(zhí)行失敗。
當事務失敗時围来,Redis 也不會進行任何的重試或者回滾動作跺涤。
一致性(Consistency)
Redis 的一致性問題可以分為三部分來討論:入隊錯誤、執(zhí)行錯誤监透、Redis 進程被終結(jié)桶错。
入隊錯誤
在命令入隊的過程中,如果客戶端向服務器發(fā)送了錯誤的命令胀蛮,比如命令的參數(shù)數(shù)量不對院刁,等等, 那么服務器將向客戶端返回一個出錯信息粪狼, 并且將客戶端的事務狀態(tài)設(shè)為 REDIS_DIRTY_EXEC 退腥。
當客戶端執(zhí)行 EXEC 命令時, Redis 會拒絕執(zhí)行狀態(tài)為 REDIS_DIRTY_EXEC 的事務鸳玩, 并返回失敗信息。
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> set key
(error) ERR wrong number of arguments for 'set' command
redis 127.0.0.1:6379> EXISTS key
QUEUED
redis 127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
因此演闭,帶有不正確入隊命令的事務不會被執(zhí)行不跟,也不會影響數(shù)據(jù)庫的一致性。
執(zhí)行錯誤
如果命令在事務執(zhí)行的過程中發(fā)生錯誤米碰,比如說窝革,對一個不同類型的 key 執(zhí)行了錯誤的操作, 那么 Redis 只會將錯誤包含在事務的結(jié)果中吕座, 這不會引起事務中斷或整個失敗虐译,不會影響已執(zhí)行事務命令的結(jié)果,也不會影響后面要執(zhí)行的事務命令吴趴, 所以它對事務的一致性也沒有影響漆诽。
Redis 進程被終結(jié)
如果 Redis 服務器進程在執(zhí)行事務的過程中被其他進程終結(jié),或者被管理員強制殺死锣枝,那么根據(jù) Redis 所使用的持久化模式厢拭,可能有以下情況出現(xiàn):
內(nèi)存模式:如果 Redis 沒有采取任何持久化機制,那么重啟之后的數(shù)據(jù)庫總是空白的撇叁,所以數(shù)據(jù)總是一致的供鸠。
RDB 模式:在執(zhí)行事務時,Redis 不會中斷事務去執(zhí)行保存 RDB 的工作陨闹,只有在事務執(zhí)行之后楞捂,保存 RDB
的工作才有可能開始薄坏。所以當 RDB 模式下的 Redis 服務器進程在事務中途被殺死時,事務內(nèi)執(zhí)行的命令寨闹,不管成功了多少胶坠,都不會被保存到
RDB 文件里”侵遥恢復數(shù)據(jù)庫需要使用現(xiàn)有的 RDB 文件涵但,而這個 RDB
文件的數(shù)據(jù)保存的是最近一次的數(shù)據(jù)庫快照(snapshot),所以它的數(shù)據(jù)可能不是最新的帖蔓,但只要 RDB
文件本身沒有因為其他問題而出錯矮瘟,那么還原后的數(shù)據(jù)庫就是一致的。-
AOF 模式:因為保存 AOF 文件的工作在后臺線程進行塑娇,所以即使是在事務執(zhí)行的中途澈侠,保存 AOF
文件的工作也可以繼續(xù)進行,因此埋酬,根據(jù)事務語句是否被寫入并保存到 AOF 文件哨啃,有以下兩種情況發(fā)生:1)如果事務語句未寫入到 AOF 文件,或 AOF 未被 SYNC 調(diào)用保存到磁盤写妥,那么當進程被殺死之后拳球,Redis
可以根據(jù)最近一次成功保存到磁盤的 AOF 文件來還原數(shù)據(jù)庫,只要 AOF
文件本身沒有因為其他問題而出錯珍特,那么還原后的數(shù)據(jù)庫總是一致的祝峻,但其中的數(shù)據(jù)不一定是最新的。2)如果事務的部分語句被寫入到 AOF 文件扎筒,并且 AOF 文件被成功保存莱找,那么不完整的事務執(zhí)行信息就會遺留在 AOF 文件里,當重啟
Redis 時嗜桌,程序會檢測到 AOF 文件并不完整奥溺,Redis 會退出,并報告錯誤骨宠。需要使用 redis-check-aof
工具將部分成功的事務命令移除之后浮定,才能再次啟動服務器。還原之后的數(shù)據(jù)總是一致的层亿,而且數(shù)據(jù)也是最新的(直到事務執(zhí)行之前為止)壶唤。
隔離性(Isolation)
Redis 是單進程程序,并且它保證在執(zhí)行事務時棕所,不會對事務進行中斷闸盔,事務可以運行直到執(zhí)行完所有事務隊列中的命令為止。因此琳省,Redis 的事務是總是帶有隔離性的迎吵。
持久性(Durability)
因為事務不過是用隊列包裹起了一組 Redis 命令躲撰,并沒有提供任何額外的持久性功能,所以事務的持久性由 Redis 所使用的持久化模式?jīng)Q定:
在單純的內(nèi)存模式下击费,事務肯定是不持久的拢蛋。
在 RDB 模式下,服務器可能在事務執(zhí)行之后蔫巩、RDB 文件更新之前的這段時間失敗谆棱,所以 RDB 模式下的 Redis 事務也是不持久的。
在 AOF 的“總是 SYNC ”模式下圆仔,事務的每條命令在執(zhí)行成功之后垃瞧,都會立即調(diào)用 fsync 或 fdatasync 將事務數(shù)據(jù)寫入到 AOF 文件。但是坪郭,這種保存是由后臺線程進行的个从,主線程不會阻塞直到保存成功,所以從命令執(zhí)行成功到數(shù)據(jù)保存到硬盤之間歪沃,還是有一段非常小的間隔嗦锐,所以這種模式下的事務也是不持久的。
其他 AOF 模式也和“總是 SYNC ”模式類似沪曙,所以它們都是不持久的奕污。
小結(jié)
- 事務提供了一種將多個命令打包,然后一次性液走、有序地執(zhí)行的機制碳默。
- 事務在執(zhí)行過程中不會被中斷,所有事務命令執(zhí)行完之后育灸,事務才能結(jié)束腻窒。
- 多個命令會被入隊到事務隊列中昵宇,然后按先進先出(FIFO)的順序執(zhí)行磅崭。
- 帶 WATCH 命令的事務會將客戶端和被監(jiān)視的鍵在數(shù)據(jù)庫的 watched_keys
字典中進行關(guān)聯(lián),當鍵被修改時瓦哎,程序會將所有監(jiān)視被修改鍵的客戶端的 REDIS_DIRTY_CAS 選項打開砸喻。 - 只有在客戶端的 REDIS_DIRTY_CAS 選項未被打開時,才能執(zhí)行事務蒋譬,否則事務直接返回失敗割岛。
- Redis 的事務保證了 ACID 中的一致性(C)和隔離性(I),但并不保證原子性(A)和持久性(D)犯助。