事务
Redis 通过MULTI EXEC WATCH 等命令来实现事务功能.
通过输入 MULTI 开启事务模式,然后输入命令链,最后执行.
19.1 事务的实现
一个事务从开始到结束通常经历一下三个阶段.
1): 事务开始.
2): 命令入队.
3): 事务执行.
19.1.1 事务开始
通过 MULTI 命令来表示事务开始
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);
}
19.1.2 命令入队
从命令的执行我们可知,当处于非事务状态 命令输入之后会立马进行反馈.
当客户端切换到事务状态时,在非 EXEC,DISCARD,WATCH,MULTI 命令,会将命令放入队列中,直到遇到上面几个命令.
/* Exec the command */
if (c->flags & REDIS_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
{
// 在事务上下文中
// 除 EXEC 、 DISCARD 、 MULTI 和 WATCH 命令之外
// 其他所有命令都会被入队到事务队列中
queueMultiCommand(c);
addReply(c,shared.queued);
} else {
// 执行命令
call(c,REDIS_CALL_FULL);
c->woff = server.master_repl_offset;
// 处理那些解除了阻塞的键
if (listLength(server.ready_keys))
handleClientsBlockedOnLists();
}
/* 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++;
}
19.1.3 事务队列
通过FIFO 的队列来保存当前事务的待执行命令.
struct redisClinet {
multiState mstate;
};
/*
* 事务命令
*/
typedef struct multiCmd {
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
} multiCmd;
19.1.4 执行事务
当一个处于事务状态的客户端想服务器发送 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);
}
19.2 WATCH 命令的实现
从Redis 的事务执行,我们不难发现这样的问题, 由于Redis 支持多客户端模式,所以说一个客户端在进入事务模式后,执行了一连串的命令,但另一个客户端对事务命令队列中的键执行了修改,那么对于之前开启事务的客户端来说,该任务队列的执行结果可能不符合自己的预期值.这个时候就要借助额外的规则进行一个约束.这就是 WATCH 命令的作用
WATCH 命令是回一个乐观锁,可以在 EXEC 命令执行之前,监视任意数量的键,在进行 EXEC 判断当前监视的键的对象是否发生了改变.
在字典中的键值对, 键由监视的字符键组成, 值则是一个链表, 表示当前键有那些客户端在 监视
struct redisDb{
//正在被 WATCH 命令监视的键
dict* watched_keys;
};
19.2.2 监视机制的触发
在进行修改命令时, 这些命令会执行
/*-----------------------------------------------------------------------------
* Hooks for key space changes.
*
* 键空间改动的钩子。
*
* Every time a key in the database is modified the function
* signalModifiedKey() is called.
*
* 每当数据库中的键被改动时, signalModifiedKey() 函数都会被调用。
*
* Every time a DB is flushed the function signalFlushDb() is called.
*
* 每当一个数据库被清空时, signalFlushDb() 都会被调用。
*----------------------------------------------------------------------------*/
void signalModifiedKey(redisDb *db, robj *key) {
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;
}
}
19.2.3 判断事务是否安全
可以在上一小节看到,当被监视的键被进行修改之后,该客户端的标记 flags 会打上 REDIS_DIRTY_CAS 标记.
所以,我应当想到,在执行 EXEC 命令时, Redis 会对 REDIS_DIRTY_CAS 标记进行判断,从而判断监视的键是否进行过更改
/* 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;
}
19.2.4 一个完成的 WATCH 事务执行过程
客户端A:
首先是对一个键进行监视
WATCH "name"
然后发起一个事务命令
MULTI
然后客户端A发起命令队列.
客户端B进行修改
SET "name" "123"
此时 客户端A输入
EXEC
事务执行失败.
19.3 事务的 ACID 性质
在 Redis 中, 事务总是具有原子性(Atomicity),一致性(Consistency)和隔离性(Isolation).并且当 Redsi 处于一种持久化模式下生,事务也具有耐久性.
19.3.1 原子性
原子性表示,一个事务要么全部执行,要么一个不执行.将命令队列中的所有指令,都当成一个命令来执行.
但是 Redis的事务和传统的关系数据库事务的最大区别在于, Redis 不支持事务回滚机制.即使某个命令在执行期间出现了错误,整个事务也会继续执行下去.
Redis 的坐着在事务功能的文档中解释,不支持事务回滚是因为这种复杂的功能和 Redis 追求简单高效的设计主旨不符.并且认为 Redis 事务的执行时错误通常是编程错误产生的,这种错误通常只会出现在开发环境中,很少在实际生产环境中出现.所以没有必要为 Redis 开发事务回滚功能
19.3.2 一致性
一致指的是数据符合数据库本身的定义和要求.没有包含非法或者无效的错误数据.
书中通过介绍三个事务可能出错的地方,并说明 Redis 是如何妥善的处理.
1: 入队错误
当收到错误命令在事务命令队列中时,事务将拒绝进行执行,所以不会收到影响
2: 执行错误
执行错误在遇到入队无法检测的错误时,会在实际执行时被触发,当然即使在执行中发生了错误,服务器也不会中断事务的执行,会继续执行事务中余下的其他命令.
该错误会在事务执行中直接被发现,也不会造成影响.
针对这个错误,我个人理解为,在逻辑的执行上,可能我们无法得到我们期望的结果,毕竟一个命令发生了错误,但是,对数据库而言,并没有什么问题,我完整的执行了你的命令,这个错误我也进行了响应.
3: 服务器停机
在事务的执行过程中发生了停机时是否能保证一致性呢.
分为两种情况:
服务器没开启持久化模式.那么重启之后服务器就是空的.所以服务器永远是一致的.
如果服务器开启了持久化模式,如果是 RDB 模式,那么也没有进行保存,如果在过程中发生宕机,将回到 RDB 文件的当时状态,那么也是一致的.
如果开启了 AOF 模式,那么在文件恢复过程中, AOF 会依次执行 AOF 文件中的命令, 也就会让事务重新运行一遍.当然也不会导致一致性出现问题.
19.3.3 隔离性
Redis 是单线程,事务是一个一个运行的,并不会产生干扰
19.3.4 耐久性
持久性是指,执行结果是否能被保存到永久性存储介质里面.即使完成后关机,执行的结果也不会丢失
当服务器在无持久化的内存模式下,事务不具有持久性,一旦停机就丢失了.
如果在RDB 持久化模式下,只有在特定的条件满足下才会进行存盘,因此也不具有耐久性
当服务器在 AOF 持久化模式下,并且 appendfsync 值为 always 时,每一条数据都会执行同步函数,进行存盘,所以有持久性.
当 appendfsync 为 everysec 时,程序会每秒同步一次命令数据到硬盘,所以可能会造成丢失,不具有持久性.
当 appendfsync 为 no 时,只有当主动发出 SAVE 命令时,才会进行存盘,所以也不具有持久性.
书中给出了一个方法,在每次事务完成后进行手动的命令存盘, SAVE 但是效率太低,但是我们可以在AOF 持久化模式下,对关键的事务进行主动存盘.
总结
事务就是一个将多种命令打包,一次性有序的执行的机制.
多个命令会被入队到失误队列中,按先进先出的顺序执行.
事务在执行过程中不会被中断.
WATCH 命令作为乐观锁,当进行监视时,被其他客户端进行更改,监视客户端的 REDIS_DIRTY_CAS 标志会被添加.
只有在客户端 REDIS_DIRTY_CAS 未被打开的时,服务器才会只进行对应的命令队列.
Rdis 的事务具有 ACID 中的原子性,一致性和隔离性, 当服务器运行在 AOF 持久化模式下,并且同步选项 appednfsync 为 always 时,事务也具有耐久性