Redis源码剖析 - Redis之事务的实现原理

Posted Fred^_^

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis源码剖析 - Redis之事务的实现原理相关的知识,希望对你有一定的参考价值。

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51262268

Redis源码剖析系列文章汇总:传送门

今天为大家带来Redis中事务部分的源码分析。Redis的事务机制允许将多个命令当做一个独立的单元运行,主要包括multi、exec、watch、unwatch、discard五个相关命令。如果你还不熟悉这几个命令,可以先看看我的另一篇文章【Redis学习笔记(七)】 Redis中的事务

本文所讲述的内容主要涉及redis.h和multi.c两个源文件,依据惯例,文后会提供注释版的源码。


1、总流程

事务从开始到执行需要经历以下三个阶段:

  1. 声明事务 (multi命令)
  2. 命令入队
  3. 执行事务(exec命令)

对于一个拥有不同状态的对象,我们通常会使用状态机的手段加以管理。Redis也使用了类似的方法来实现对事务中不同状态的管理。

我们先来看看与事务有关的几个状态,这在Redis中又称作flag,定义在redis.h中。

#define REDIS_MULTI (1<<3)   
#define REDIS_DIRTY_EXEC (1<<12)
#define REDIS_DIRTY_CAS (1<<5) 

下面具体介绍一下各个flag的含义:

  1. REDIS_MULTI表示客户端处于事务状态。当客户端执行multi命令后便由非事务状态转变为事务状态。在非事务状态下命令是一个接一个按序执行的;而当客户端处于事务状态时,命令则以事务为单位执行,一次性执行事务队列中的所有命令。
  2. REDIS_DIRTY_EXEC表示EXEC无效状态。当客户端进入事务状态后,Redis等待接收一个或多个命令,并把它们放入命令队列中等待执行。如果某条命令在入队过程中发生错误则进入该状态,此时Redis将客户端的flags标识字段置为REDIS_DIRTY_EXEC,随后的EXEC命令将会失败返回。
  3. REDIS_DIRTY_CAS表示非安全状态,该状态是针对watch命令设置的,客户端可以在声明事务前使用watch命令对一个或多个key进行监视,如果在事务执行之前这些被监视的key被其他命令修改,则进入REDIS_DIRTY_CAS状态。因为此时将要执行事务所相关的key被修改,无法保证事务的原子性。REDIS_DIRTY_CAS状态下如果执行exec命令也会失败返回,即相当于该事务被取消。

这里说明一下,上面的各个状态是我根据自己的理解定义的,便于理解事务执行流程,但可能有不规范之处

所以Redis的事务整个流程大致是这样的:

  1. 客户端redisClient中有一个名叫flags的成员,标识当前客户端的状态。
  2. 在声明事务之前,我们可以通过watch命令对一个或多个key进行监视。如果在事务执行之前这些被监视的key被其他命令修改,Redis将redisClient->flags设置为REDIS_DIRTY_CAS标识。
  3. 使用multi命令可以标识着一个事务的开始,此时redisClient进入事务状态,其flags字段被设置为REDIS_MULTI标识。
  4. 当客户端进入事务状态后,Redis服务器等待接收一个或多个命令,并把它们放入命令队列中等待执行。如果某条命令在入队过程中发生错误,Redis会将redisClient的flags字段置为REDIS_DIRTY_EXEC标识。
  5. 最后我们通过exec命令执行事务,该命令将会检查redisClient的flags标识,如果该标识为REDIS_DIRTY_CAS或REDIS_DIRTY_EXEC,则事务执行失败,否则Redis一次性执行事务中的多个命令,并将所有命令的结果集合到回复队列,再作为 exec 命令的结果返回给客户端。

2、watch命令实现

与watch命令有关的关键数据结构主要有两个:

首先,每个redisDb数据库使用一个哈希表来维护key和所有监控该key的客户端列表的映射关系。这样当一个key被修改后,我们就可以对所有监控该key的客户端设置dirty标识。

redisDb结构定义在redis.h头文件中,这里省略其它无关代码。

/* Redis数据库结构体 */
typedef struct redisDb 
    ...
    // 被watch命令监控的键和相应client
    dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
    int id;
    ...
 redisDb;

watched_keys为字典(哈希表)结构,键为被监视的key,值为所有监控该key的客户端列表(即list数据结构)。正如下所示:

另外,每个客户端redisClient也维护着一个保存所有被监控的key的列表,这样就可以方便地对key取消监视。这个列表就是redisClient中的watched_keys成员,该成员是一个双向链表list结构。

在redisClient->watched_keys中使用watchedKey结构来标识一个Redis中的key,在watchedKey中不仅需要保存被监视的key,还需要记录该key所在的数据库。其定义如下:

typedef struct watchedKey 
    // 被监控的key
    robj *key;
    // key所在的数据库
    redisDb *db;
 watchedKey;

redisDb-> watched_keys和 redisClient->watched_keys两者的结合就实现了watch/unwatch命令所需要的功能:通过redisDb-> watched_keys 哈希表, 如果某个程序需要检查某个键是否被监视, 那么它只要检查字典中是否存在这个键即可; 通过redisClient->watched_keys,如果某个程序要获得该客户端监视的所有key,那么它只要获得该链表即可。

接下来,我们看看watch命令的具体实现,该功能由watchForKey函数完成。

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是否已经保存在client->watched_keys列表中

    // listRewind获取list的迭代器
    listRewind(c->watched_keys,&li);
    // 遍历查找,如果发现给定key已经存在直接返回
    while((ln = listNext(&li))) 
        wk = listNodeValue(ln);
        if (wk->db == c->db && equalStringObjects(key,wk->key))
            return; /* Key already watched */
    

    /* This key is not already watched in this DB. Let's add it */
    // 检查redisDB->watched_keys是否保存了该key和客户端的映射关系,如果没有则添加之
    // 获取监控给定key的客户端列表
    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结构添加到client->watched_keys列表中
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);

unwatch命令执行相反的操作,由unwatchAllKeys函数实现。

void unwatchAllKeys(redisClient *c) 
    listIter li;
    listNode *ln;

    // 如果没有key被监控,直接返回
    if (listLength(c->watched_keys) == 0) return;
    // 获得c->watched_keys列表的迭代器
    listRewind(c->watched_keys,&li);
    // 遍历c->watched_keys列表,逐一删除被该客户端监视的key
    while((ln = listNext(&li))) 
        list *clients;
        watchedKey *wk;

        /* Lookup the watched key -> clients list and remove the client
         * from the list */
        wk = listNodeValue(ln);
        // 将当前客户端从db->watched_keys中删除
        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 */
        // 如果没有任何客户端监控该key,则将该key从db->watched_keys中删除
        if (listLength(clients) == 0)
            dictDelete(wk->db->watched_keys, wk->key);

        /* Remove this watched key from the client->watched list */
        // 将c->watched_keys删除该key
        listDelNode(c->watched_keys,ln);

        // 释放资源
        decrRefCount(wk->key);
        zfree(wk);
    

对于上面这两段代码,我已经详细注释过了,这里就不展开讲解。接下来我们看看multi/exec命令实现原理。

3、multi/exec命令实现

我们从 “声明事务 ” => “命令入队” => “执行事务”这三个阶段来分别介绍其原理。

3.1、声明事务

声明事务通过multi命令实现,从下面源码中我们可以看到:

  1. Redis不支持嵌套事务。
  2. 声明事务其实就是简单地将flags设置为REDIS_MULTI标识。随后redisClient进入事务状态,等待命令入队。
/* 执行MULTI命令 */
void multiCommand(redisClient *c) 
    // 不支持嵌套事务,否则直接报错
    if (c->flags & REDIS_MULTI) 
        addReplyError(c,"MULTI calls can not be nested");
        return;
    
    // 设置事务标识
    c->flags |= REDIS_MULTI;
    addReply(c,shared.ok);

3.2、命令入队

这里我们先来介绍一下与命令队列相关的数据结构。

在redisClient中存在multiState mstate字段用来保存一个事务中的所有命令和其它相关信息,multiState结构定义在redis.h头文件中。

/* 事务状态结构体 */
typedef struct multiState 
    // 命令数组,保存着该事务中的所有命令并按输入顺序排列
    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;

multiState结构中的multiCmd *commands正是真正存放命令的命令队列,其实质是一个数组。multiCmd表示一条完整的输入命令,包含“要执行的命令”、“命令参数”、“参数个数”三个属性。定义如下:

/* 事务命令结构体 */
typedef struct multiCmd 
    // 命令参数
    robj **argv;
    // 参数个数
    int argc;
    // 要执行的命令
    struct redisCommand *cmd;
 multiCmd;

命令入队由queueMultiCommand函数实现。

/* 将一个新命令添加到multi命令队列中 */
void queueMultiCommand(redisClient *c) 
    multiCmd *mc;
    int j;

    // 在原commands后面配置空间以存放新命令
    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]);
    // 命令队列中保存的命令个数加1
    c->mstate.count++;

3.3、执行事务

Redis通过exec命令执行事务,该命令将会检查redisClient的flags标识,如果该标识为REDIS_DIRTY_CAS或REDIS_DIRTY_EXEC,则事务执行失败返回。如果客户端仍然处于事务状态, 那么当 exec 命令执行时,Redis会根据客户端所保存的事务队列, 以“先近先出”的策略执行事务队列中的命令,即最先入队的命令最先执行, 而最后入队的命令最后执行。

exec命令由execCommand执行。

/* 执行exec命令 */
void execCommand(redisClient *c) 
    int j;
    robj **orig_argv;
    int orig_argc;
    struct redisCommand *orig_cmd;
    // 是否需要将MULTI/EXEC命令传播到slave节点/AOF
    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.
     * 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. */
    // 检查是否需要中断事务执行,因为:
    // (1)、有被监控的key被修改
    // (2)、命令入队的时候发生错误
    //  对于第一种情况,Redis返回多个nil空对象(准确地说这种情况并不是错误,应视为一种特殊的行为)
    //  对于第二种情况则返回一个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 */
    // 现在可以执行该事务的所有命令了

    // 取消对所有key的监控,否则会浪费CPU资源
    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);
    // 逐一将事务中的命令交给客户端redisClient执行
    for (j = 0; j < c->mstate.count; j++) 
        // 将事务命令队列中的命令设置给客户端
        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.
         * 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. */
        //  当我们第一次遇到写命令时,传播MULTI命令。如果是读命令则无需传播
        //  这里我们MULTI/..../EXEC当做一个整体传输,保证服务器和AOF以及附属节点的一致性
        if (!must_propagate && !(c->cmd->flags & REDIS_CMD_READONLY)) 
            execCommandPropagateMulti(c);
            // 只需要传播一次MULTI命令即可
            must_propagate = 1;
        

        // 真正执行命令
        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;
    
    // 恢复原命令
    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. */
    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);

Redis事务的实现原理就介绍这么多。很多人一听到“事务”这个词就会潜意识的认为这是一个很复杂的东西。而实际上Redis中使用很轻巧的办法提供事务操作,代码只有300来行,并不是很复杂。

注释版源码请移步:https://github.com/xiejingfa/the-annotated-redis-2.8.24

以上是关于Redis源码剖析 - Redis之事务的实现原理的主要内容,如果未能解决你的问题,请参考以下文章

Redis源码剖析 - Redis之数据库redisDb

Redis源码剖析 - Redis IO操作之rio

Redis源码剖析--双端链表Sdlist

Redis源码剖析 - Redis内置数据结构之字符串sds

Redis源码剖析之AOF

Redis源码剖析 - Redis数据类型之列表List