事务定义:将多个命令打包,然后一次性、按顺序执行多个命令。在执行命令期间(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标识来决定是否执行事务。如果打开了说明至少有一个键被修改过了,事务不再安全,会拒绝执行事务,反之则执行事务。