【Redis源码剖析】

来源:互联网 发布:c语言单链表反转 编辑:程序博客网 时间:2024/06/01 10:25

今天为大家带来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