redis事务的原理介绍在redis设计与实现这本书中已经讲述的非常清楚,这里就不浪费更多的时间去讲述了。这篇博客的目的主要还是结合源码来讲述redis事务的实现。如果大家对redis有点熟悉,都知道redis客户端和服务器之间的通信都是通过命令+key+值来进行通信的(内部协议的格式在以后的博客中讲述)。redis的事务也是通过命令来实现的。
MULTI 事务的开始
EXEC 按顺序执行事务中的命令
DISCARD 取消事务
这三个命令实现事务的基本功能。除了这三个命令还有个WATCH,WATCH命令的作用在redis设计与实现中也有详细的讲述,这里也就不浪费口舌了。
在正式讲述事务之前,先来介绍两个结构体:
typedef struct multiCmd {
// 执行命令的所有 key 对象
robj **argv;
// 参数的数量
int argc;
// 被执行的命令
struct redisCommand *cmd;
} multiCmd;
/*
* 事务状态结构
*/
typedef struct multiState {
// 数组,保存着所有在事务队列中的命令
multiCmd *commands; /* Array of MULTI commands */
// 队列中命令的数量
int count; /* Total number of MULTI commands */
} multiState;
上面两个结构体就是关于事务的信息。上面都有足够多的解释,这里也就不一一讲述了。现在让我们来从事务开始来讲起,也就是MULTI对应的函数:multiCommand
void multiCommand(redisClient *c) {
// MULTI 命令不能嵌套
if (c->flags & REDIS_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
// 打开事务的 FLAG
// 从此之后,除 DISCARD 和 EXEC 等少数几个命令之外
// 其他所有的命令都会被添加到事务队列里
c->flags |= REDIS_MULTI;
addReply(c,shared.ok);
}
从上面的源码来看,这个函数比较简单。最主要还是下面这句话:
c->flags |= REDIS_MULTI;
c是一个redisClient类型的数据,redisClient就是对应着当前操作数据库的客户端的信息。所以把当前客户端设置为REDIS_MULTI也就说明当前客户端处于事务状态。然后就立马发送OK给客户端。那具体插入命令到事务中是如何操作的呢?来看下processCommand这个函数的实现:
int processCommand(redisClient *c) {
.....
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);
// 每次执行完命令之后,处理所有就绪列表
if (listLength(server.ready_keys))
handleClientsBlockedOnLists();
}
.....
}
processCommand的一些预处理就直接省略了,看上面的代码,如果当前的客户端处于事务状态并且命令不是MULTI,DISCARD,EXEC,WATCH都会把当前的命令插入到队列中,并且发送QUEUED给客户端,否则客户端不处于事务状态,那就是redis正常的处理流程。下面来看下queueMultiCommand
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的时候,redis服务器是怎么操作的呢?当redis服务器收到EXEC命令后也会经过processCommand,上面已经讲述过如果是EXEC,MULTI,DISCARD,WATCH这四种命令时,是不会入队列的,而是立马执行。与EXEC对应的函数是execCommand,下面来看下这个函数:
void execCommand(redisClient *c) {
......
if (!(c->flags & REDIS_MULTI)) {
addReplyError(c,"EXEC without MULTI");
return;
}
......
if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
// 根据状态,决定返回的错误的类型
addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
shared.nullmultibulk);
// 以下四句可以用 discardTransaction() 来替换
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);
unwatchAllKeys(c);
goto handle_monitor;
}
......
for (j = 0; j < c->mstate.count; j++) {
// 因为 call 可能修改命令,而命令需要传送给其他同步节点
// 所以这里将要执行的命令(及其参数)先备份起来
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;
}
......
}
函数刚开始就会判断客户端是不是处于事务状态,如果不在事务状态是不能执行EXEC这个命令的。
还有就是要注意,如果当一个客户端处于事务状态,并且操作了一些key,但是这个key被另外一个客户端修改,并且前一个客户端还处于事务状态,这时事务状态的客户端会执行事务失败。
下面的for循环就是把命令从队列中取出再一个个执行,这里的逻辑也比较简单也就不详述了。
自此,redis事务的开始以及事务命令入队列以及取出命令执行都讲完了,至于DISCARD和WATCH就让大家自己去看吧。