概述

本文讲述redis的事务的实现原理。

基本概念

事务(Transactions)

事务是一系列命令的集合,这个命令的集合要么全部执行成功,全部执行失败。redis事务类似于传统数据库的事务,但不同的是:若事务执行过程中发生错误,redis的事务不支持回滚。

事务的使用

事务的使用通过multi命令开始,exec命令结束。

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

注意:可以调用discard命令来刷新事务队列,退出事务。

事务(Transactions)的实现分析

multi命令的实现

该命令的实现流程如下:

  1. 判断当前客户端是否正在执行事务,若有事务正在执行,向客户端返回"事务不能嵌套"的错误。
  2. 在当前客户端设置REDIS_MULTI标识,表示该客户端正在执行事务。
  3. 返回设置成功的状态。

该命令对应的执行函数是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函数处理事务的流程如下:

  1. 若client已经设置了REDIS_MULTI 标志,且本次执行的命令不是:EXEC,DISCARD,MULTI,WATCH这四者之一,进入事务过程的命令处理。
  2. 调用queueMultiCommand函数来处理事务过程中的命令。
  3. 该函数会把命令保存到客户端结构的一个命令数组中,该命令的实现代码如下:
/* 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++;
}
  1. 在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;
    ...
}

从上面的代码分析可以得到以下结论:

  1. multi命令执行后,再输入常规命令,是不会执行的,而是会把命令保存到客户端结构的命令数组中。
  2. 在这个过程中可以使用discard命令放弃事务,也可以使用exec执行已经放入命令队列的命令列表。

下面我们来看exec命令的实现。

exec命令的实现

该命令用来执行事务的命令列表,该命令的实现流程如下:

  1. 首先检查是否已经设置了REDIS_MULTI标志,若没有设置给客户端返回错误,退出命令。
  2. 此时若设置了REDIS_DIRTY_CAS或REDIS_DIRTY_EXEC标记,则拒绝执行事务的命令。
    说明:若watch 了某个key,就会把该key放到watched_keys字典中,在对该key做任何修改操作时,客户端都设置REDIS_DIRTY_CAS标志位。
  3. 检查是否要放弃EXEC命令。若发生以下的情况,将会放弃EXEC命令:
  1. 有些key,被WATCH。
  2. 在执行事务过程中,前面有命令执行出错。
  1. 遍历事务命令表,一条一条执行对应函数

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);
}

该函数的主要实现流程如下:

  1. 调用freeClientMultiState函数释放已保存在客户端的命令数据结构,回收内存
  2. 调用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事务机制的实现。并对源码进行了分析。