概述
本文讲述redis的事务的实现原理。
基本概念
事务(Transactions)
事务是一系列命令的集合,这个命令的集合要么全部执行成功,全部执行失败。redis事务类似于传统数据库的事务,但不同的是:若事务执行过程中发生错误,redis的事务不支持回滚。
事务的使用
事务的使用通过multi命令开始,exec命令结束。
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
注意:可以调用discard命令来刷新事务队列,退出事务。
事务(Transactions)的实现分析
multi命令的实现
该命令的实现流程如下:
- 判断当前客户端是否正在执行事务,若有事务正在执行,向客户端返回"事务不能嵌套"的错误。
- 在当前客户端设置REDIS_MULTI标识,表示该客户端正在执行事务。
- 返回设置成功的状态。
该命令对应的执行函数是multiCommand,代码如下:
void multiCommand(redisClient *c) {
if (c->flags & REDIS_MULTI) {
// 若已经设置了REDIS_MULTI标记说明有事务正在该客户端执行,直接返回错误。
addReplyError(c,"MULTI calls can not be nested");
return;
}
// 在client结构的flags字段,设置REDIS_MULTI标记。
c->flags |= REDIS_MULTI;
// 向客户端返回成功状态
addReply(c,shared.ok);
}
小结:MULTI命令的实现十分简单,就是在client结构的flag字段上设置REDIS_MULTI标志。
事务过程中的命令执行
在redis中会通过函数int processCommand(client *c)来处理命令。当设置了multi后,客户端就打上了事务执行的标记。这样redis在处理后面的命令时,将会进行特殊的处理。
我们来分析一下当执行了multi命令后(也就是客户端设置了REDIS_MULTI标识)后,再执行常规命令,processCommand函数是如何处理的。
processCommand函数处理事务的流程如下:
- 若client已经设置了REDIS_MULTI 标志,且本次执行的命令不是:EXEC,DISCARD,MULTI,WATCH这四者之一,进入事务过程的命令处理。
- 调用queueMultiCommand函数来处理事务过程中的命令。
- 该函数会把命令保存到客户端结构的一个命令数组中,该命令的实现代码如下:
/* Add a new command into the MULTI commands queue */
void queueMultiCommand(client *c) {
multiCmd *mc;
int j;
// 在原来的基础上再申请一块内存,大小为:sizeof(multiCmd)
c->mstate.commands = zrealloc(c->mstate.commands,
sizeof(multiCmd)*(c->mstate.count+1));
// 所有的命令是保存在一个c->msstate.commands这个数组中,这是一个multiCmd结构的数组。
mc = c->mstate.commands+c->mstate.count; //定位到申请的那块内存起始位置
// 设置该内存的块(multiCmd结构)的值
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]);
// multi中的命令数量+1
c->mstate.count++;
}
- 在processCommand函数中,处理事务的代码如下:
int processCommand(client *c) {
...
/* Exec the command */
// 若client已经设置了REDIS_MULTI 标志,
// 且本次执行的命令不是:EXEC,DISCARD,MULTI,WATCH这四者之一。
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);
} else {
// 若没有设置REDIS_MULTI标志,则执行命令对应的函数。
call(c,CMD_CALL_FULL);
c->woff = server.master_repl_offset;
if (listLength(server.ready_keys))
handleClientsBlockedOnLists();
}
return C_OK;
...
}
从上面的代码分析可以得到以下结论:
- multi命令执行后,再输入常规命令,是不会执行的,而是会把命令保存到客户端结构的命令数组中。
- 在这个过程中可以使用discard命令放弃事务,也可以使用exec执行已经放入命令队列的命令列表。
下面我们来看exec命令的实现。
exec命令的实现
该命令用来执行事务的命令列表,该命令的实现流程如下:
- 首先检查是否已经设置了REDIS_MULTI标志,若没有设置给客户端返回错误,退出命令。
- 此时若设置了REDIS_DIRTY_CAS或REDIS_DIRTY_EXEC标记,则拒绝执行事务的命令。
说明:若watch 了某个key,就会把该key放到watched_keys字典中,在对该key做任何修改操作时,客户端都设置REDIS_DIRTY_CAS标志位。 - 检查是否要放弃EXEC命令。若发生以下的情况,将会放弃EXEC命令:
- 有些key,被WATCH。
- 在执行事务过程中,前面有命令执行出错。
- 遍历事务命令表,一条一条执行对应函数
discard命令的实现
discard命令是通过discardCommand函数来实现的,该函数会调用discardTransaction函数。
- discard命令的主要功能就是清除客户端的事务队列中的命令,并清除客户端的REDIS_MULTI标识。注意:是清楚所有的事务中的命令,而不只是一个命令。
void discardCommand(client *c) {
if (!(c->flags & CLIENT_MULTI)) {
addReplyError(c,"DISCARD without MULTI");
return;
}
discardTransaction(c); // 见下面的分析
addReply(c,shared.ok);
}
- discardTransaction函数的实现代码如下:
void discardTransaction(client *c) {
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(CLIENT_MULTI|CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC);
unwatchAllKeys(c);
}
该函数的主要实现流程如下:
- 调用freeClientMultiState函数释放已保存在客户端的命令数据结构,回收内存
- 调用initClientMultiState重置客户端的事务相关的数据结构,代码如下:
void initClientMultiState(client *c) {
c->mstate.commands = NULL;
c->mstate.count = 0;
}
事务中的竞争条件
在执行事务中的命令时,若还有其他的客户端也在操作同样的key的话,key的值可能会产生不一致的情况,这种情况该如何解决呢?
redis提供了一种watch机制,通过watch机制可以监视某个key,若在执行exec命令之前修改了至少一个watch的key,则整个事务将会终止,且exec将会返回null来通知该事务失败了。这种watch机制,其实是一种乐观锁(check and set)机制,关于watch机制的实现,将会在下一篇文章中进行分析。
总结
本文分析了redis事务机制的实现。并对源码进行了分析。