事務是一個數據庫必備的元素,對於redis也不例外,對於一個傳統的關系型數據庫來說,數據庫事務滿足ACID四個特性:
- A代表原子性:一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
- C代表一致性:事務應確保數據庫的狀態從一個一致狀態轉變為另一個一致狀態。一致狀態的含義是數據庫中的數據應滿足完整性約束
- I代表隔離性:多個事務並發執行時,一個事務的執行不應影響其他事務的執行
- D代表持久性:已被提交的事務對數據庫的修改應該永久保存在數據庫中
然而,對於redis來說,只滿足其中的:
一致性和隔離性兩個特性,其他特性是不支持的。
關於redis對ACID四個特性暫時先說這么多,在本文后面會詳細說明。在詳述之前,我們先來了解redis事務具體的使用和實現,這樣我們接下來討論ACID時才能更好的理解。
redis事務的使用
redis事務主要包括MULTI、EXEC、DISCARD和WATCH命令
MULTI命令
MULTI命令用來開啟一個事務,當MULTI執行之后,客戶端可以繼續向服務器發送多條命令,這些命令會緩存在隊列里面,只有當執行EXEC命令時,這些命令才會執行。
而DISCARD命令可以清空事務隊列,放棄執行事務。
一個使用MULTI和EXEC執行事務的例子:
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"
一個事務從開始到執行會經歷以下三個階段:
- 開始事務。
- 命令入隊。
- 執行事務。
redis事務產生錯誤
使用redis事務可能會產生錯誤,主要分為兩大類:
- 事務在執行EXEC之前,入隊的命令可能出錯。
- 命令可能在 EXEC 調用之后失敗
redis在2.6.5之后,如果發現事務在執行EXEC之前出現錯誤,那么會放棄這個事務。
在EXEC命令之后產生的錯誤,會被忽略,其他正確的命令會被繼續執行。
redis事務不支持回滾
如果你有使用關系式數據庫的經驗, 那么 “Redis 在事務失敗時不進行回滾,而是繼續執行余下的命令”這種做法可能會讓你覺得有點奇怪。
以下是這種做法的優點:
- Redis 命令只會因為錯誤的語法而失敗(並且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來說,失敗的命令是由編程錯誤造成的,而這些錯誤應該在開發的過程中被發現,而不應該出現在生產環境中。
- 因為不需要對回滾進行支持,所以 Redis 的內部可以保持簡單且快速。 有種觀點認為 Redis 處理事務的做法會產生 bug , 然而需要注意的是, 在通常情況下, 回滾並不能解決編程錯誤帶來的問題。 舉個例子, 如果你本來想通過 INCR 命令將鍵的值加上 1 , 卻不小心加上了 2 , 又或者對錯誤類型的鍵執行了 INCR , 回滾是沒有辦法處理這些情況的。
鑒於沒有任何機制能避免程序員自己造成的錯誤, 並且這類錯誤通常不會在生產環境中出現, 所以 Redis 選擇了更簡單、更快速的無回滾方式來處理事務。
關於WATCH命令
WATCH命令可以添加監控的鍵,如果這些監控的鍵沒有被其他客戶端修改,那么事務可以順利執行,如果被修改了,那么事務就不能執行。
redis> WATCH name OK redis> MULTI OK redis> SET name peter QUEUED redis> EXEC (nil)
以下執行序列展示了上面的例子是如何失敗的:
| 時間 | 客戶端 A | 客戶端 B |
|---|---|---|
| T1 | WATCH name |
|
| T2 | MULTI |
|
| T3 | SET name peter |
|
| T4 | SET name john |
|
| T5 | EXEC |
在時間 T4 ,客戶端 B 修改了 name 鍵的值, 當客戶端 A 在 T5 執行 EXEC 時,Redis 會發現 name 這個被監視的鍵已經被修改, 因此客戶端 A 的事務不會被執行,而是直接返回失敗
當客戶端發送 EXEC 命令、觸發事務執行時, 服務器會對客戶端的狀態進行檢查:
- 如果客戶端的
REDIS_DIRTY_CAS選項已經被打開,那么說明被客戶端監視的鍵至少有一個已經被修改了,事務的安全性已經被破壞。服務器會放棄執行這個事務,直接向客戶端返回空回復,表示事務執行失敗。 - 如果
REDIS_DIRTY_CAS選項沒有被打開,那么說明所有監視鍵都安全,服務器正式執行事務。
這里是偽代碼
def check_safety_before_execute_trasaction():
if client.state & REDIS_DIRTY_CAS:
# 安全性已破壞,清除事務狀態
clear_transaction_state(client)
# 清空事務隊列
clear_transaction_queue(client)
# 返回空回復給客戶端
send_empty_reply(client)
else:
# 安全性完好,執行事務
execute_transaction()
redis事務的實現
在了解了redis事務的使用之后,我們再來看看redis事務的實現,主要是對上面說的MULTI、EXEC、DISCARD和WATCH命令的源碼的分析。
MULTI命令實現
void multiCommand(redisClient *c) {
if (c->flags & REDIS_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
c->flags |= REDIS_MULTI;
addReply(c,shared.ok);
}
從上面的源碼可以看出,MULTI只能執行一次,而且就做一件事,把客戶端的標志打上REDIS_MULTI。
EXEC命令實現
void execCommand(redisClient *c) {
int j;
robj **orig_argv;
int orig_argc;
struct redisCommand *orig_cmd;
if (!(c->flags & REDIS_MULTI)) {
addReplyError(c,"EXEC without MULTI");
return;
}
/* Check if we need to abort the EXEC if some WATCHed key was touched.
* A failed EXEC will return a multi bulk nil object. */
if (c->flags & REDIS_DIRTY_CAS) {
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
unwatchAllKeys(c);
addReply(c,shared.nullmultibulk);
goto handle_monitor;
}
/* Replicate a MULTI request now that we are sure the block is executed.
* This way we'll deliver the MULTI/..../EXEC block as a whole and
* both the AOF and the replication link will have the same consistency
* and atomicity guarantees. */
execCommandReplicateMulti(c);
/* Exec all the queued commands */
unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
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;
call(c,REDIS_CALL_FULL);
/* Commands may alter argc/argv, restore mstate. */
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;
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
/* Make sure the EXEC command is always replicated / AOF, since we
* always send the MULTI command (we can't know beforehand if the
* next operations will contain at least a modification to the DB). */
server.dirty++;
handle_monitor:
/* Send EXEC to clients waiting data from MONITOR. We do it here
* since the natural order of commands execution is actually:
* MUTLI, EXEC, ... commands inside transaction ...
* Instead EXEC is flagged as REDIS_CMD_SKIP_MONITOR in the command
* table, and we do it here with correct ordering. */
if (listLength(server.monitors) && !server.loading)
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}
其主要步驟是:
- 檢查是否已經是處在MULTI狀態下,如果不是,直接返回
- 檢查WATCH的key是否已經被其他客戶端修改,如果是,放棄事務
- 執行事務命令隊列里面的所有命令
執行命令隊列里面的所有命令的代碼如下:
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;
call(c,REDIS_CALL_FULL);
/* Commands may alter argc/argv, restore mstate. */
c->mstate.commands[j].argc = c->argc;
c->mstate.commands[j].argv = c->argv;
c->mstate.commands[j].cmd = c->cmd;
}
遍歷queue隊列的所有命令,一條條的執行
DISCARD命令實現
DICARD命令主要由下面兩個函數完成:
void discardCommand(redisClient *c) {
if (!(c->flags & REDIS_MULTI)) {
addReplyError(c,"DISCARD without MULTI");
return;
}
discardTransaction(c);
addReply(c,shared.ok);
}
void discardTransaction(redisClient *c) {
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);;
unwatchAllKeys(c);
}
void freeClientMultiState(redisClient *c) {
int j;
for (j = 0; j < c->mstate.count; j++) {
int i;
multiCmd *mc = c->mstate.commands+j;
for (i = 0; i < mc->argc; i++)
decrRefCount(mc->argv[i]);
zfree(mc->argv);
}
zfree(c->mstate.commands);
}
函數調用的關系如下
discardCommand
|
|
|
discardTransaction
|
|
|
freeClientMultiState
freeClientMultiState就是完成DISCARD命令主要功能的函數,把其命令隊列里面的所有命令所在的存儲空間都釋放。
WATCH命令
void watchForKey(redisClient *c, robj *key) {
list *clients = NULL;
listIter li;
listNode *ln;
watchedKey *wk;
/* Check if we are already watching for this key */
listRewind(c->watched_keys,&li);
while((ln = listNext(&li))) {
wk = listNodeValue(ln);
if (wk->db == c->db && equalStringObjects(key,wk->key))
return; /* Key already watched */
}
/* This key is not already watched in this DB. Let's add it */
clients = dictFetchValue(c->db->watched_keys,key);
if (!clients) {
clients = listCreate();
dictAdd(c->db->watched_keys,key,clients);
incrRefCount(key);
}
listAddNodeTail(clients,c);
/* Add the new key to the lits of keys watched by this client */
wk = zmalloc(sizeof(*wk));
wk->key = key;
wk->db = c->db;
incrRefCount(key);
listAddNodeTail(c->watched_keys,wk);
}
這個函數是在WATCH命令執行后,主要完成兩件事情:
- 把key添加到當前客戶端c->watched_keys的鏈表中,
- 並且把當前客戶端添加到c->db->watched_keys的字典里面去。
當db中有能修改鍵狀態的命令(比如INCR等)執行時,會自動把watch這個key的客戶端的事務設置為放棄狀態:
void touchWatchedKey(redisDb *db, robj *key) {
list *clients;
listIter li;
listNode *ln;
if (dictSize(db->watched_keys) == 0) return;
clients = dictFetchValue(db->watched_keys, key);
if (!clients) return;
/* Mark all the clients watching this key as REDIS_DIRTY_CAS */
/* Check if we are already watching for this key */
listRewind(clients,&li);
while((ln = listNext(&li))) {
redisClient *c = listNodeValue(ln);
c->flags |= REDIS_DIRTY_CAS;
}
}
如上源碼所示,當某個key被修改后,會遭到其字典中所在的hash鏈表,然后把鏈表中所有的客戶端都設置成REDIS_DIRTY_CAS狀態,即事務被DISCARD的狀態。
redis事務的ACID性質討論
A原子性
單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增加任何維持原子性的機制,所以 Redis 事務的執行並不是原子性的
如果一個事務隊列中的所有命令都被成功地執行,那么稱這個事務執行成功
另一方面,如果 Redis 服務器進程在執行事務的過程中被停止 —— 比如接到 KILL 信號、宿主機器停機,等等,那么事務執行失敗
事務失敗時,Redis 也不會進行任何的重試或者回滾動作,不滿足要么全部全部執行,要么都不執行的條件
C一致性
一致性分下面幾種情況來討論:
首先,如果一個事務的指令全部被執行,那么數據庫的狀態是滿足數據庫完整性約束的
其次,如果一個事務中有的指令有錯誤,那么數據庫的狀態是滿足數據完整性約束的
最后,如果事務運行到某條指令時,進程被kill掉了,那么要分下面幾種情況討論:
- 如果當前redis采用的是內存模式,那么重啟之后redis數據庫是空的,那么滿足一致性條件
-
如果當前采用RDB模式存儲的,在執行事務時,Redis 不會中斷事務去執行保存 RDB 的工作,只有在事務執行之后,保存 RDB 的工作才有可能開始。所以當 RDB 模式下的 Redis 服務器進程在事 務中途被殺死時,事務內執行的命令,不管成功了多少,都不會被保存到 RDB 文件里。 恢復數據庫需要使用現有的 RDB 文件,而這個 RDB 文件的數據保存的是最近一次的數 據庫快照(snapshot),所以它的數據可能不是最新的,但只要 RDB 文件本身沒有因為 其他問題而出錯,那么還原后的數據庫就是一致的
-
如果當前采用的是AOF存儲的,那么可能事務的內容還未寫入到AOF文件,那么此時肯定是滿足一致性的,如果事務的內容有部分寫入到AOF文件中,那么需要用工具把AOF中事務執行部分成功的指令移除,這時,移除之后的AOF文件也是滿足一致性的
所以,redis事務滿足一致性約束
I隔離性
Redis 是單進程程序,並且它保證在執行事務時,不會對事務進行中斷,事務可以運行直到執行完所有事務隊列中的命令為止。因此,Redis 的事務是總是帶有隔離性的。
D持久性
因為事務不過是用隊列包裹起了一組 Redis 命令,並沒有提供任何額外的持久性功能,所以事務的持久性由 Redis 所使用的持久化模式決定
- 在單純的內存模式下,事務肯定是不持久的
- 在 RDB 模式下,服務器可能在事務執行之后、RDB 文件更新之前的這段時間失敗,所以 RDB 模式下的 Redis 事務也是不持久的
- 在 AOF 的“總是 SYNC ”模式下,事務的每條命令在執行成功之后,都會立即調用
fsync或fdatasync將事務數據寫入到 AOF 文件。但是,這種保存是由后台線程進行的,主線程不會阻塞直到保存成功,所以從命令執行成功到數據保存到硬盤之間,還是有一段非常小的間隔,所以這種模式下的事務也是不持久的。 - 其他 AOF 模式也和“總是 SYNC ”模式類似,所以它們都是不持久的。
來源:https://github.com/Charles0429/redis_doc/blob/master/function/redis_transaction.md
http://redisbook.readthedocs.org/en/latest/feature/transaction.html
