一:簡介
Redis事務通常會使用MULTI,EXEC,WATCH等命令來完成,redis實現事務實現的機制與常見的關系型數據庫有很大的卻別,比如redis的事務不支持回滾,事務執行時會阻塞其它客戶端的請求執行。
二:事務實現細節
redis事務從開始到結束通常會通過三個階段:
1.事務開始
2.命令入隊
3.事務執行
我們從下面的例子看下
redis > MULTI
OK
redis > SET "username" "bugall"
QUEUED
redis > SET "password" 161616
QUEUED
redis > GET "username"
redis > EXEC
1) ok
2) "bugall"
3) "bugall"
redis > MULTI
標記事務的開始,MULTI命令可以將執行該命令的客戶端從非事務狀態切換成事務狀態,這一切換是通過在客戶端狀態的flags屬性中打開REDIS_MULTI標識完成,我們看下redis中對應部分的源碼實現
void multiCommand(client *c) {
if (c->flags & CLIENT_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
c->flags |= CLIENT_MULTI; //打開事務標識
addReply(c,shared.ok);
}
在打開事務標識的客戶端里,這些命令,都會被暫存到一個命令隊列里,不會因為用戶會的輸入而立即執行
redis > SET "username" "bugall"
redis > SET "password" 161616
redis > GET "username"
執行事務隊列里的命令。
redis > EXEC
這里需要注意的是,在客戶端打開了事務標識后,只有命令:EXEC,DISCARD,WATCH,MULTI命令會被立即執行,其它命令服務器不會立即執行,而是將這些命令放入到一個事務隊列里面,然后向客戶端返回一個QUEUED回復,redis客戶端有自己的事務狀態,這個狀態保存在客戶端狀態mstate屬性中,mstate的結構體類型是multiState,我們看下multiState的定義
typedef struct multiState {
multiCmd *commands; //存放MULTI commands的數組
int count; //命令數量
} multiState;
我們再看下結構體類型multiCmd的結構
typedef struct multiCmd {
robj **argv; //參數
int argc; //參數數量
struct redisCommand *cmd; //命令指針
} multiCmd;
事務隊列以先進先出的保存方法,較先入隊的命令會被放到數組的前面,而較后入隊的命令則會被放到數組的后面.
三:執行事務
當開啟事務標識的客戶端發送EXEC命令的時候,服務器就會執行,客戶端對應的事務隊列里的命令,我們來看下EXEC的實現細節
void execCommand(client *c) {
int j;
robj **orig_argv;
int orig_argc;
struct redisCommand *orig_cmd;
int must_propagate = 0; //同步持久化,同步主從節點
//如果客戶端沒有開啟事務標識
if (!(c->flags & CLIENT_MULTI)) {
addReplyError(c,"EXEC without MULTI");
return;
}
//檢查是否需要放棄EXEC
//如果某些被watch的key被修改了就放棄執行
if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :
shared.nullmultibulk);
discardTransaction(c);
goto handle_monitor;
}
//執行事務隊列里的命令
unwatchAllKeys(c); //因為redis是單線程的所以這里,當檢測watch的key沒有被修改后就統一clear掉所有的watch
orig_argv = c->argv;
orig_argc = c->argc;
orig_cmd = c->cmd;
addReplyMultiBulkLen(c,c->mstate.count);
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;
//同步主從節點,和持久化
if (!must_propagate && !(c->cmd->flags & CMD_READONLY)) {
execCommandPropagateMulti(c);
must_propagate = 1;
}
//執行命令
call(c,CMD_CALL_FULL);
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;
//取消客戶端的事務標識
discardTransaction(c);
if (must_propagate) server.dirty++;
handle_monitor:
if (listLength(server.monitors) && !server.loading)
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}
四:watch/unwatch/discard
watch:
命令是一個樂觀鎖,它可以在EXEC命令執行之前,監視任意數量的數據庫鍵,並在執行EXEC命令時判斷是否至少有一個被watch的鍵值
被修改如果被修改就放棄事務的執行,如果沒有被修改就清空watch的信息,執行事務列表里的命令。
unwatch:
顧名思義可以看出它的功能是與watch相反的,是取消對一個鍵值的“監聽”的功能能
discard:
清空客戶端的事務隊列里的所有命令,並取消客戶端的事務標記,如果客戶端在執行事務的時候watch了一些鍵,則discard會取消所有
鍵的watch.
五:redis事務的ACID特性
在傳統的關系型數據庫中,常常用ACID特質來檢測事務功能的可靠性和安全性。
在redis中事務總是具有原子性(Atomicity),一致性(Consistency)和隔離性(Isolation),並且當redis運行在某種特定的持久化模式下,事務也具有耐久性(Durability).
1 原子性
事務具有原子性指的是,數據庫將事務中的多個操作當作一個整體來執行,服務器要么就執行事務中的所有操作,要么就一個操作也不執行。
但是對於redis的事務功能來說,事務隊列中的命令要么就全部執行,要么就一個都不執行,因此redis的事務是具有原子性的。我們通常會知道兩種關於redis事務原子性的說法,一種是要么事務都執行,要么都不執行。另外一種說法是redis事務當事務中的命令執行失敗后面的命令還會執行,錯誤之前的命令不會回滾。其實這個兩個說法都是正確的。但是缺一不可。我們接下來具體分析下
我們先看一個可以正確執行的事務例子
redis > MULTI
OK
redis > SET username "bugall"
QUEUED
redis > EXEC
1) OK
2) "bugall"
與之相反,我們再來看一個事務執行失敗的例子。這個事務因為命令在放入事務隊列的時候被服務器拒絕,所以事務中的所有命令都不會執行,因為前面我們有介紹到,redis的事務命令是統一先放到事務隊列里,在用戶輸入EXEC命令的時候再統一執行。但是我們錯誤的使用"GET"命令,在命令放入事務隊列的時候被檢測到事務,這時候還沒有接收到EXEC命令,所以這個時候不牽扯到回滾的問題,在EXEC的時候發現事務隊列里有命令存在錯誤,所以事務里的命令就全都不執行,這樣就達到了事務的原子性,我們看下例子。
redis > MULTI
OK
redis > GET
(error) ERR wrong number of arguments for 'get' command
redis > GET username
QUEUED
redis > EXEC
(error) EXECABORT Transaction discarded because of previous errors
redis的事務和傳統的關系型數據庫事務的最大區別在於,redis不支持事務的回滾機制,即使事務隊列中的某個命令在執行期間出現錯誤,整個事務也會繼續執行下去,直到將事務隊列中的所有命令都執行完畢為止,我們看下面的例子
redis > SET username "bugall"
OK
redis > MULTI
OK
redis > SADD member "bugall" "litengfe" "yangyifang"
QUEUED
redis > RPUSH username "b" "l" "y" //錯誤對鍵username使用列表鍵命令
QUEUED
redis > SADD password "123456" "123456" "123456"
QUEUED
redis > EXEC
1) (integer) 3
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 3
redis的作者在十五功能的文檔中解釋說,不支持事務回滾是因為這種復雜的功能和redis追求的簡單高效的設計主旨不符合,並且他認為,redis事務的執行時
錯誤通常都是編程錯誤造成的,這種錯誤通常只會出現在開發環境中,而很少會在實際的生產環境中出現,所以他認為沒有必要為redis開發事務回滾功能。所以
我們在討論redis事務回滾的時候,一定要區分命令發生錯誤的時候。
2 一致性
事務具有一致性指的是,如果數據庫在執行事務之前是一致的,那么在事務執行之后,無論事務是否執行成功,數據庫也應該仍然一致的。
”一致“指的是數據符合數據庫本身的定義和要求,沒有包含非法或者無效的錯誤數據。redis通過謹慎的錯誤檢測和簡單的設計來保證事務一致性。
3 隔離性
事務的隔離性指的是,即使數據庫中有多個事務並發在執行,各個事務之間也不會互相影響,並且在並發狀態下執行的事務和串行執行的事務產生的結果完全
相同。
因為redis使用單線程的方式來執行事務(以及事務隊列中的命令),並且服務器保證,在執行事務期間不會對事物進行中斷,因此,redis的事務總是以串行
的方式運行的,並且事務也總是具有隔離性的
4 持久性
事務的耐久性指的是,當一個事務執行完畢時,執行這個事務所得的結果已經被保持到永久存儲介質里面。
因為redis事務不過是簡單的用隊列包裹起來一組redis命令,redis並沒有為事務提供任何額外的持久化功能,所以redis事務的耐久性由redis使用的模式
決定
- 當服務器在無持久化的內存模式下運行時,事務不具有耐久性,一旦服務器停機,包括事務數據在內的所有服務器數據都將丟失
- 當服務器在RDB持久化模式下運作的時候,服務器只會在特定的保存條件滿足的時候才會執行BGSAVE命令,對數據庫進行保存操作,並且異步執行的BGSAVE不
能保證事務數據被第一時間保存到硬盤里面,因此RDB持久化模式下的事務也不具有耐久性 - 當服務器運行在AOF持久化模式下,並且appedfsync的選項的值為always時,程序總會在執行命令之后調用同步函數,將命令數據真正的保存到硬盤里面,因此
這種配置下的事務是具有耐久性的。 - 當服務器運行在AOF持久化模式下,並且appedfsync的選項的值為everysec時,程序會每秒同步一次命令數據到磁盤因為停機可能會恰好發生在等待同步的那一秒內,這種可能造成事務數據丟失,所以這種配置下的事務不具有耐久性
