事務定義:將多個命令打包,然後一次性、按順序執行多個命令。在執行命令期間(EXEC),不會中斷事務而去執行其他客戶端的命令請求。滿足ACID中的原子性、一致性和隔離性。
舉個例子:
redis 127.0.0.1:6379> MULTI --------------事務開始命令
OK
redis 127.0.0.1:6379> SET name wqh --------------命令1入隊
QUEUED --------------命令1回覆
redis 127.0.0.1:6379> GET name --------------命令2入隊
QUEUED --------------命令2回覆
redis 127.0.0.1:6379> EXEC --------------事務執行命令
1) OK --------------事務統一回復
2) "wqh"
MULTI命令標誌着事務的開始,該命令將客戶端從非事務狀態切換到事務狀態,在客戶端狀態的flags屬性中,打開標識REDIS_MULTI(1<<3)。源碼如下:
void multiCommand(redisClient *c) {
// 不能在事務中嵌套事務
if (c->flags & REDIS_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
// 打開事務 FLAG
c->flags |= REDIS_MULTI;
addReply(c,shared.ok);
}
注意的是,不能在事務中嵌套着其他事務,不同事務之間相互獨立,串行執行。
命令入隊:當客戶端處於事務狀態下,當執行除了EXEC、DISCARD、WATCH、MULTI(立刻執行)這四個命令以外的其他命令,服務器將不會立即執行該命令,而且放入事務隊列中,向客戶端返回QUEUED恢復。
Redis客戶端的事務狀態是保存在mstate狀態中:
typedef struct redisClient {
// ...
// 事務狀態
multiState mstate; /* MULTI/EXEC state */
// ...
}
事務狀態定義爲:
/*
* 事務狀態
*/
typedef struct multiState {
// 事務隊列,FIFO 順序
multiCmd *commands; /* Array of MULTI commands */
// 已入隊命令計數
int count; /* Total number of MULTI commands */
int minreplicas; /* MINREPLICAS for synchronous replication */
time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;
可以看出,事無狀態主要由一個事務隊列和一個已入隊命令的計數器組成。事務隊列以先進先出(FIFO)方式進行,是一個multiCmd類型的數組,即事務命令,該類型定義爲:
/*
* 事務命令
*/
typedef struct multiCmd {
// 參數
robj **argv;
// 參數數量
int argc;
// 命令指針
struct redisCommand *cmd;
} multiCmd;
將一個新命令添加到事務隊列中的源碼:
/* Add a new command into the MULTI commands queue
*
* 將一個新命令添加到事務隊列中
*/
void queueMultiCommand(redisClient *c) {
multiCmd *mc;
int j;
// 爲新數組元素分配空間
c->mstate.commands = zrealloc(c->mstate.commands,
sizeof(multiCmd)*(c->mstate.count+1));
// 指向新元素
mc = c->mstate.commands+c->mstate.count;
// 設置事務的命令、命令參數數量,以及命令的參數
mc->cmd = c->cmd;
mc->argc = c->argc;
mc->argv = zmalloc(sizeof(robj*)*c->argc);
memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);
for (j = 0; j < c->argc; j++)
incrRefCount(mc->argv[j]);
// 事務命令數量計數器增一
c->mstate.count++;
}
執行事務:執行EXEC命令,服務器遍歷這個客戶端的事務隊列,執行所有命令,最後將執行結果全部返回給客戶端。該命令的執行源碼爲:
void execCommand(redisClient *c) {
int j;
robj **orig_argv;
int orig_argc;
struct redisCommand *orig_cmd;
int must_propagate = 0; /* Need to propagate MULTI/EXEC to AOF / slaves? */
// 客戶端沒有執行事務
if (!(c->flags & REDIS_MULTI)) {
addReplyError(c,"EXEC without MULTI");
return;
}
/* Check if we need to abort the EXEC because:
*
* 檢查是否需要阻止事務執行,因爲:
*
* 1) Some WATCHed key was touched.
* 有被監視的鍵已經被修改了
*
* 2) There was a previous error while queueing commands.
* 命令在入隊時發生錯誤
* (注意這個行爲是 2.6.4 以後才修改的,之前是靜默處理入隊出錯命令)
*
* A failed EXEC in the first case returns a multi bulk nil object
* (technically it is not an error but a special behavior), while
* in the second an EXECABORT error is returned.
*
* 第一種情況返回多個批量回復的空對象
* 而第二種情況則返回一個 EXECABORT 錯誤
*/
if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
shared.nullmultibulk);
// 取消事務
discardTransaction(c);
goto handle_monitor;
}
/* 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++) {
// 因爲 Redis 的命令必須在客戶端的上下文中執行
// 所以要將事務隊列中的命令、命令參數等設置給客戶端
c->argc = c->mstate.commands[j].argc;
c->argv = c->mstate.commands[j].argv;
c->cmd = c->mstate.commands[j].cmd;
/* Propagate a MULTI request once we encounter the first write op.
*
* 當遇上第一個寫命令時,傳播 MULTI 命令。
*
* 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.
*
* 這可以確保服務器和 AOF 文件以及附屬節點的數據一致性。
*/
if (!must_propagate && !(c->cmd->flags & REDIS_CMD_READONLY)) {
// 傳播 MULTI 命令
execCommandPropagateMulti(c);
// 計數器,只發送一次
must_propagate = 1;
}
// 執行命令
call(c,REDIS_CALL_FULL);
/* Commands may alter argc/argv, restore mstate. */
// 因爲執行後命令、命令參數可能會被改變
// 比如 SPOP 會被改寫爲 SREM
// 所以這裏需要更新事務隊列中的命令和參數
// 確保附屬節點和 AOF 的數據一致性
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);
/* Make sure the EXEC command will be propagated as well if MULTI
* was already propagated. */
// 將服務器設爲髒,確保 EXEC 命令也會被傳播
if (must_propagate) 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);
}
Watch命令實現
Watch命令,樂觀鎖,可以實現對任意數量的數據庫鍵的監視,在EXEC命令執行時,會檢查被監視的鍵是否至少有一個已經被修改過了,如果是則拒絕執行事務,並返回空回覆nil。
watch命令的執行:不能在事務開始後執行
void watchCommand(redisClient *c) {
int j;
// 不能在事務開始後執行
if (c->flags & REDIS_MULTI) {
addReplyError(c,"WATCH inside MULTI is not allowed");
return;
}
// 監視輸入的任意個鍵
for (j = 1; j < c->argc; j++)
addReply(c,shared.ok);
}
void unwatchCommand(redisClient *c) {
// 取消客戶端對所有鍵的監視
unwatchAllKeys(c);
// 重置狀態
c->flags &= (~REDIS_DIRTY_CAS);
addReply(c,shared.ok);
}
監視一個鍵的定義爲:typedef struct watchedKey {
// 被監視的鍵
robj *key;
// 鍵所在的數據庫
redisDb *db;
} watchedKey;
可以看出,包含被監視的鍵和該鍵所在的數據庫。
每個數據庫redisDb都保存着一個watched_keys字典,字典的鍵是被WATCH命令所監視的某個鍵,值是一個鏈表,該鏈表記錄着所有監視該鍵的客戶端。
typedef struct redisDb {
// 正在被 WATCH 命令監視的鍵
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
}
客戶端狀態下也存放着被該客戶端監視的鍵的鏈表集合。
typedef struct redisClient {
// 被監視的鍵
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
}
因此有些操作需要同時對上述兩個屬性進行操作:
(1)客戶端監視指定鍵:
/* Watch for the specified key
*
* 讓客戶端 c 監視給定的鍵 key
*/
void watchForKey(redisClient *c, robj *key) {
list *clients = NULL;
listIter li;
listNode *ln;
watchedKey *wk;
/* Check if we are already watching for this key */
// 檢查 key 是否已經保存在 watched_keys 鏈表中,
// 如果是的話,直接返回
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 */
}
// 鍵不存在於 watched_keys ,添加它
// 以下是一個 key 不存在於字典的例子:
// before :
// {
// 'key-1' : [c1, c2, c3],
// 'key-2' : [c1, c2],
// }
// after c-10086 WATCH key-1 and key-3:
// {
// 'key-1' : [c1, c2, c3, c-10086],
// 'key-2' : [c1, c2],
// 'key-3' : [c-10086]
// }
/* This key is not already watched in this DB. Let's add it */
// 檢查 key 是否存在於數據庫的 watched_keys 字典中
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 list of keys watched by this client */
// 將新 watchedKey 結構添加到客戶端 watched_keys 鏈表的表尾
// 以下是一個添加 watchedKey 結構的例子
// before:
// [
// {
// 'key': 'key-1',
// 'db' : 0
// }
// ]
// after client watch key-123321 in db 0:
// [
// {
// 'key': 'key-1',
// 'db' : 0
// }
// ,
// {
// 'key': 'key-123321',
// 'db': 0
// }
// ]
wk = zmalloc(sizeof(*wk));
wk->key = key;
wk->db = c->db;
incrRefCount(key);
listAddNodeTail(c->watched_keys,wk);
}
(2)取消客戶端對所有鍵的監視:
/* Unwatch all the keys watched by this client. To clean the EXEC dirty
* flag is up to the caller.
*
* 取消客戶端對所有鍵的監視。
*
* 清除客戶端事務狀態的任務由調用者執行。
*/
void unwatchAllKeys(redisClient *c) {
listIter li;
listNode *ln;
// 沒有鍵被監視,直接返回
if (listLength(c->watched_keys) == 0) return;
// 遍歷鏈表中所有被客戶端監視的鍵
listRewind(c->watched_keys,&li);
while((ln = listNext(&li))) {
list *clients;
watchedKey *wk;
/* Lookup the watched key -> clients list and remove the client
* from the list */
// 從數據庫的 watched_keys 字典的 key 鍵中
// 刪除鏈表裏包含的客戶端節點
wk = listNodeValue(ln);
// 取出客戶端鏈表
clients = dictFetchValue(wk->db->watched_keys, wk->key);
redisAssertWithInfo(c,NULL,clients != NULL);
// 刪除鏈表中的客戶端節點
listDelNode(clients,listSearchKey(clients,c));
/* Kill the entry at all if this was the only client */
// 如果鏈表已經被清空,那麼刪除這個鍵
if (listLength(clients) == 0)
dictDelete(wk->db->watched_keys, wk->key);
/* Remove this watched key from the client->watched list */
// 從鏈表中移除 key 節點
listDelNode(c->watched_keys,ln);
decrRefCount(wk->key);
zfree(wk);
}
}
監視機制的觸發:
所有對客戶端執行修改的命令,如SET,LPUSH,FLUSHDB等,執行之後都會調用相關函數對watched_keys字典進行檢查,查看被該客戶端監視的鍵是否被上述命令修改過,如果是,則監視被修改鍵的客戶端的REDIS_DIRTY_CAS標識(1<<5)打開,表明該客戶端的事務安全性已經被破壞。
這裏面,標識被打開的函數有兩個:
(1)touchWatchedkey(db, key)
/* "Touch" a key, so that if this key is being WATCHed by some client the
* next EXEC will fail.
*
* “觸碰”一個鍵,如果這個鍵正在被某個/某些客戶端監視着,
* 那麼這個/這些客戶端在執行 EXEC 時事務將失敗。
*/
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 */
// 遍歷所有客戶端,打開他們的 REDIS_DIRTY_CAS 標識
listRewind(clients,&li);
while((ln = listNext(&li))) {
redisClient *c = listNodeValue(ln);
c->flags |= REDIS_DIRTY_CAS;
}
}
(2)touchWatchedKeysOnFlush(dbid):FLUSHDB/FLUSHALL命令
/* On FLUSHDB or FLUSHALL all the watched keys that are present before the
* flush but will be deleted as effect of the flushing operation should
* be touched. "dbid" is the DB that's getting the flush. -1 if it is
* a FLUSHALL operation (all the DBs flushed).
*
* 當一個數據庫被 FLUSHDB 或者 FLUSHALL 清空時,
* 它數據庫內的所有 key 都應該被觸碰。
*
* dbid 參數指定要被 FLUSH 的數據庫。
*
* 如果 dbid 爲 -1 ,那麼表示執行的是 FLUSHALL ,
* 所有數據庫都將被 FLUSH
*/
void touchWatchedKeysOnFlush(int dbid) {
listIter li1, li2;
listNode *ln;
// 這裏的思路挺有趣的,不是遍歷數據庫的所有 key 來讓客戶端變爲 DIRTY
// 而是遍歷所有客戶端,然後遍歷客戶端監視的鍵,再讓相應的客戶端變爲 DIRTY
// 後者要比前者高效很多
/* For every client, check all the waited keys */
// 遍歷所有客戶端
listRewind(server.clients,&li1);
while((ln = listNext(&li1))) {
redisClient *c = listNodeValue(ln);
// 遍歷客戶端監視的鍵
listRewind(c->watched_keys,&li2);
while((ln = listNext(&li2))) {
// 取出監視的鍵和鍵的數據庫
watchedKey *wk = listNodeValue(ln);
/* For every watched key matching the specified DB, if the
* key exists, mark the client as dirty, as the key will be
* removed. */
// 如果數據庫號碼相同,或者執行的命令爲 FLUSHALL
// 那麼將客戶端設置爲 REDIS_DIRTY_CAS
if (dbid == -1 || wk->db->id == dbid) {
if (dictFind(wk->db->dict, wk->key->ptr) != NULL)
c->flags |= REDIS_DIRTY_CAS;
}
}
}
}
當服務接收到一個客戶端發來的EXEC命令後,首先會檢測這個客戶端是否打開了REDIS_DIRTY_CAS標識來決定是否執行事務。如果打開了說明至少有一個鍵被修改過了,事務不再安全,會拒絕執行事務,反之則執行事務。