寫在前面
Redis 通過 MULTI
、EXEC
、WATCH
等命令來實現事務功能。Redis的事務是將多個命令請求打包,然后一次性、按照順序的執行多個命令的機制,并且在事務執行期間,服務器不會中斷事務而該去執行其他客戶端的命令請求。
就像下面這樣:
redis> MULTI
OK
redis(TX)> SET fanone 1
QUEUED
redis(TX)> SET fantwo 2
QUEUED
redis(TX)> GET fanone
QUEUED
redis(TX)> EXEC
1) OK
2) OK
3) "1"
本文我們就從redis的事務執行過程以及ACID四個方面來介紹redis的事務
事務實現
從上面的例子我們可以知道,redis的事務是從MULTI命令
開始的,所有的命令都會按照FIFO的順序
進入一個QUEUE隊列中,當執行EXEC操作
后才將這些命令逐步執行。
MULTI
事務隊列是一個multiCmd類型
的數組,數組中的每個multiCmd結構都保存了一個已入隊命令的相關信息,包括指向命令實現函數的指針、命令的參數,以及參數的數量:
typedef struct multiCmd{robj **argv; // 參數int argc; // 參數數量struct redisCommand *cmd // 命令指針
} multiCmd;
事務隊列以FIFO先進先出
的方式保存入隊的命令,還是用上面的例子來畫一個原型圖:
當一個處于事務狀態的客戶端向服務器發送EXEC命令的時候,這個EXEC命令將立即被服務器執行。服務器會遍歷這個客戶端的事務隊列,執行隊列中保存的所有命令,最后將執行命令所得的結果全部返回給客戶端
。EXEC的偽代碼如下:
void EXEC() {std::vector<Reply> reply_queue; // 創建空白的回復隊列for (const auto& cmd : client.mstate.commands) { // 執行事務中的所有命令Reply reply = execute_command(cmd.command, cmd.argv, cmd.argc);reply_queue.push_back(reply);}client.flags &= ~REDIS_MULTI; // 清理事務狀態client.mstate.count = 0;release_transaction_queue(client.mstate.commands);send_reply_to_client(client, reply_queue);// 發送回復給客戶端
}
WATCH
接著我們來講講WATCH命令,其實這是一個樂觀鎖(optimistic locking)
,可以在EXEC命令執行之前,監視任意數量的數據庫鍵,并在EXEC命令執行時,檢查被監視的鍵是否有已經被修改過了的,如果是的話,服務器將拒絕執行事務,并向客戶端返回代表事務執行失敗的空回復
。
比如下面這個例子:
時間 | 客戶端A | 客戶端B |
---|---|---|
T1 | WATCH “fanone” | |
T2 | MULTI | |
T3 | SET “fanone” 1 | |
T4 | SET “fanone” 2 | |
T5 | EXEC |
而在時間T4,客戶端B修改了fanone鍵的值,當客戶端A在T5執行EXEC命令的時候,服務器會發現WATCH監視的鍵fanone已經被修改了,因此服務器拒絕執行客戶端A的事務,并且向客戶端A返回空回復
。
每個Redis數據庫都保存著一個watched_keys字典,這個字典的鍵是某個被WATCH命令監視的數據庫鍵,而字典的值則是一個鏈表,鏈表中記錄了所有監視數據庫鍵的客戶端。
typedef struct redisDb {dict *watched_keys; // 正在被WATCH命令監視的鍵
}
ACID
Redis的事務是否符合ACID呢?
原子性 Atomicity
事務具有原子性是指,數據庫將事務中的多個操作當作一個整體來執行,要么全部執行,要么全部不執行。對于Redis的事務功能來說,事務隊列中的命令要么就都全部都執行,要么就一個都不執行,因此Redis的事務是具有原子性的。
比如以下成功執行的事務,事務中的所有命令都被執行:
redis> MULTI
OK
redis(TX)> SET fanone 1
QUEUED
redis(TX)> GET fanone
QUEUED
redis(TX)> EXEC
1) OK
2) "1"
與此相反,如果其中有一個命令是錯誤的,那么整個命令就不會執行。fanone這個key是一個string,并不是set,所以不能使用SADD命令執行。
redis> MULTI
OK
redis(TX)> SET fanone 1
QUEUED
redis(TX)> SADD fanone 2
QUEUED
redis(TX)> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
那么大家會發現Redis的事務和傳統的關系型數據庫事務的最大區別在于,Redis不支持事務回滾機制,也就是rollback。
Redis的作者在事務功能的文檔中也解釋道,不支持事務回滾是因為這種復雜的功能和Redis追求簡單高效的設計主旨不相符。
一致性 Consistency
事務具有一致性指的是,如果數據庫在執行事務之前是一致的,那么在事務執行之后,無論事務是否執行成功,數據庫也應該依然是一致的。
而這個一致指的是 數據符合數據庫本身的定義和要求,沒有包含非法或者無效的錯誤數據
比如執行完事務不會多一個之前沒有的命令,或者某個key是string類型,不會變成set類型。我們用上面同一個例子來說明:
redis> MULTI
OK
redis(TX)> SET fanone 1
QUEUED
redis(TX)> SADD fanone 2
QUEUED
redis(TX)> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
fanone 這個key是string類型,但是無法通過SADD命令將fanone變成set類型。雖然在入隊列的時候,redis沒有報錯,但是在EXEC的時候,redis報了,所以這個key一開始是string類型,事務執行完后也會是string類型,事務執行前后保持了一致。
隔離性 Isolation
事務的隔離型是指**即使數據庫中有多個事務并發的執行,各個事務之間也不會互相影響,并且在并發狀態下執行的事務和穿行執行的事務的結果是完成相同的。
**
因為Redis是使用單線程的方式來執行事務以及事務隊列中的命令,并且在服務器穩定的情況下,執行事務不會中斷,因此,redis的事務總是串行的方式執行的,所以具備隔離性。
持久性 Durability
事務的持久性值得是當一個事務執行完畢的時候,執行這個事務所得的結果已經被保存到永久性存儲介質里面了,即使服務器在事務執行完畢之后停機,執行事務所得的結果也不會丟失。
由于Redis的事務比較簡潔,沒有提供持久化的能力,所以Redis的事務是依賴于Redis所使用的持久話模式,也就是AOF、RDB,我們一個個來討論
-
當服務器
無持久化運行的時候,事務不具備持久性
,一旦服務器宕機,事務數據將會丟失。 -
當服務器在RDB持久化模式下運作的時候,服務器只會在特定的保存條件下滿足,比如使用BGSAVE命令,隊數據庫進行保存,但是異步執行的BGSAVE也不能保證,第一時間保存在硬盤中,
因此RDB持久化模式下事務不具備持久性
。
-
而當服務器運行
在AOF持久話模式下,并且appendfsync選項是always的時候,服務器總會在執行完命令之后調用同步sync函數,將命令數據保存在硬盤中,而此時事務具備持久性
,其他選擇比如everysec或者no的時候都不具備持久性。