Redis支持事務機制,但Redis的事務機制與傳統關系型數據庫的事務機制並不相同。
Redis事務的本質是一組命令的集合(命令隊列)。事務可以一次執行多個命令,並提供以下保證:
(1)事務中的所有命令都按順序執行。事務命令執行過程中,其他客戶端提交的命令請求需要等待當前事務所有命令執行完成后再處理,不會插入當前事務命令隊列中。
(2)事務中的命令要么都執行,要么都不執行,即使事務中有些命令執行失敗,后續命令依然被執行。因此Redis事務也是原子的。
注意Redis不支持回滾,如果事務中有命令執行失敗了,那么Redis會繼續執行后續命令而不是回滾。
可能有讀者疑惑Redis是否支持ACID?筆者認為,ACID概念起源於傳統的關系型數據庫,而Redis是非關系型數據庫,而且Redis並沒有聲明是否支持ACID,所以本文不討論該問題。
事務的應用示例
Redis提供了MULTI、EXEC、DISCARD和WATCH命令來實現事務功能:
> MULTI
OK
> SET points 1
QUEUED
> INCR points
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
- MULTI命令可以開啟一個事務,后續的命令都會被放入事務命令隊列。
- EXEC命令可以執行事務命令隊列中的所有命令,DISCARD命令可以拋棄事務命令隊列中的命令,這兩個命令都會結束當前事務。
- WATCH命令可以監視指定鍵,當后續事務執行前發現這些鍵已修改時,則拒絕執行事務。
表17-1展示了一個WATCH命令的簡單使用示例。
可以看到,在執行EXEC命令前如果WATCH的鍵被修改,則EXEC命令不會執行事務,因此WATCH常用於實現樂觀鎖。
事務的實現原理
server.h/multiState結構體負責存放事務信息:
typedef struct multiState {
multiCmd *commands;
...
} multiState;
- commands:事務命令隊列,存放當前事務所有的命令。
客戶端屬性client.mstate指向一個multiState變量,該multiState作為客戶端的事務上下文,負責存放該客戶端當前的事務信息。
下面看一下MULTI、EXEC和WATCH命令的實現。
WATCH命令的實現
提示:本章代碼如無特殊說明,均在multi.c中。
WATCH命令的實現邏輯較獨立,我們先分析該命令的實現邏輯。
redisDb中定義了字典屬性watched_keys,該字典的鍵是數據庫中被監視的Redis鍵,字典的值是監視字典鍵的所有客戶端列表,如圖17-1所示。
client中也定義了列表屬性watched_keys,記錄該客戶端所有監視的鍵。
watchCommand函數負責處理WATCH命令,該函數會調用watchForKey函數處理相關邏輯:
void watchForKey(client *c, robj *key) {
...
// [1]
clients = dictFetchValue(c->db->watched_keys,key);
...
listAddNodeTail(clients,c);
// [2]
wk = zmalloc(sizeof(*wk));
wk->key = key;
wk->db = c->db;
incrRefCount(key);
listAddNodeTail(c->watched_keys,wk);
}
【1】將客戶端添加到redisDb.watched_keys字典中該Redis鍵對應的客戶端列表中。
【2】初始化watchedKey結構體(wk變量),該結構體可以存儲被監視鍵和對應的數據庫。 將wk變量添加到client.watched_keys中。
Redis中每次修改數據時,都會調用signalModifiedKey函數,將該數據標志為已修改。
signalModifiedKey函數會調用touchWatchedKey函數,通知監視該鍵的客戶端數據已修改:
void touchWatchedKey(redisDb *db, robj *key) {
...
clients = dictFetchValue(db->watched_keys, key);
if (!clients) return;
listRewind(clients,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags |= CLIENT_DIRTY_CAS;
}
}
從redisDb.wzatched_keys中獲取所有監視該鍵的客戶端,給這些客戶端添加CLIENT_ DIRTY_CAS標志,該標志代表客戶端監視的鍵已被修改。
MULTI、EXEC命令的實現
MULTI命令由multiCommand函數處理,該函數的處理非常簡單,就是打開客戶端CLIENT_MULTI標志,代表該客戶端已開啟事務。
前面說過,processCommand函數執行命令時,會檢查客戶端是否已開啟事務。如果客戶端已開啟事務,則調用queueMultiCommand函數,將命令請求添加到客戶端事務命令隊列client.mstate.commands中:
int processCommand(client *c) {
...
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
{
queueMultiCommand(c);
addReply(c,shared.queued);
} ...
return C_OK;
}
可以看到,如果當前客戶端開啟了事務,則除了MULTI、EXEC、DISCARD和WATCH命令,其他命令都會放入到事務命令隊列中。
EXEC命令由execCommand函數處理:
void execCommand(client *c) {
...
// [1]
if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr : shared.nullarray[c->resp]);
discardTransaction(c);
goto handle_monitor;
}
// [2]
unwatchAllKeys(c);
...
addReplyArrayLen(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;
// [3]
if (!must_propagate &&
!server.loading &&
!(c->cmd->flags & (CMD_READONLY|CMD_ADMIN)))
{
execCommandPropagateMulti(c);
must_propagate = 1;
}
// [4]
int acl_keypos;
int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
if (acl_retval != ACL_OK) {
...
} else {
call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);
}
...
}
// [5]
...
discardTransaction(c);
// [6]
if (must_propagate) {
int is_master = server.masterhost == NULL;
server.dirty++;
...
}
...
}
【1】當客戶端監視的鍵被修改(客戶端存在CLIENT_DIRTY_CAS標志)或者客戶端已拒絕事務中的命令(客戶端存在CLIENT_DIRTY_EXEC標志)時,直接拋棄事務命令隊列中的命令,並進行錯誤處理。
當服務器處於異常狀態(如內存溢出)時,Redis將拒絕命令,並給開啟了事務的客戶端添加CLIENT_DIRTY_EXEC標志。
【2】取消當前客戶端對所有鍵的監視,所以WATCH命令只能作用於后續的一個事務。
【3】在執行事務的第一個寫命令之前,傳播MULTI命令到AOF文件和從節點。MULTI命令執行完后並不會被傳播(MULTI命令並不屬於寫命令),如果事務中執行了寫命令,則在這里傳播MULTI命令。
【4】檢查用戶的ACL權限,檢查通過后執行命令。
【5】執行完所有命令,調用discardTransaction函數重置客戶端事務上下文client.mstate,並刪除CLIENT_MULTI、CLIENT_DIRTY_CAS、CLIENT_DIRTY_EXEC標志,代表當前事務已經處理完成。
【6】如果事務中執行了寫命令,則修改server.dirty,這樣會使server.c/call函數將EXEC命令傳播到AOF文件和從節點,從而保證一個事務的MULTI、EXEC命令都被傳播。
關於Redis不支持回滾機制,Redis在官網中給出了如下解釋:
(1)僅當使用了錯誤語法(並且該錯誤無法在命令加入隊列期間檢測)或者Redis命令操作數據類型錯誤(比如對集合類型使用了HGET命令)時,才可能導致事務中的命令執行失敗,這意味着事務中失敗的命令是編程錯誤的結果,所以這些問題應該在開發過程中發現並處理,而不是依賴於在生產環境中的回滾機制來規避。
(2)不支持回滾,Redis事務機制實現更簡單並且性能更高。
Redis的事務非常簡單,即在一個原子操作內執行多條命令。Redis的Lua腳本也是事務性的,所以用戶也可以使用Lua腳本實現事務。Redis Lua腳本會在后續章節詳細分析。
總結:
- Redis事務保證多條命令在一個原子操作內執行。
- Redis提供了MULTI、EXEC、DISCARD和WATCH命令來實現事務功能。
- 使用WATCH命令可以實現樂觀鎖機制。
本文內容摘自作者新書《Redis核心原理與實踐》。本書通過深入分析Redis 6.0源碼,總結了Redis核心功能的設計與實現。通過閱讀本書,讀者可以深入理解Redis內部機制及最新特性,並學習到Redis相關的數據結構與算法、Unix編程、存儲系統設計,分布式系統架構等一系列知識。
經過該書編輯同意,我會繼續在個人技術公眾號(binecy)發布書中部分章節內容,作為書的預覽內容,歡迎大家查閱,謝謝。
語雀平台預覽:《Redis核心原理與實踐》
京東鏈接