前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Redis | 事务源码阅读

Redis | 事务源码阅读

作者头像
码农UP2U
发布2021-01-04 14:47:11
发布2021-01-04 14:47:11
46400
代码可运行
举报
文章被收录于专栏:码农UP2U码农UP2U
运行总次数:0
代码可运行

Redis 的事务在前面的内容中已经介绍过了,分别对应的两篇文章如下:

Redis | Redis 的事务一

Redis | Redis 的事务二

因为 Redis 的特性是要快,因此 Redis 事务没有关系型数据库的事务那么强大,这是它们在设计上的权衡。Redis 的事务只有简单的几条命令,且较为简单。它的实现也相对简单许多。我们来阅读一下 Redis 关于事务的源码。

事务的命令

事务的命令只有为数不多的几个,比较常用的是 multi、discard 和 exec 三个命令。这三个命令的作用分别是 开启事务、取消事务 和 执行事务在事务中还有两个命令用来提供乐观锁,分别是 watch 和 unwatch 两个命令。这些命令在前面的文章中已经进行过整理。这篇文章重点来整理事务相关的源码。

multi 命令

multi 命令的源码在 multi.c 文件中,且源码特别的短,源码如下。

代码语言:javascript
代码运行次数:0
复制
/**
 * multi命令对应的源码
 */void multiCommand(redisClient *c) {    // 判断是否嵌套执行multi
    if(c->flags & REDIS_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");
        return;
    }

    // 开启multi标志位
    c->flags |= REDIS_MULTI;
    addReply(c,shared.ok);}

multiCommand 函数中只完成了两个功能,第一个功能是判断 multi 命令是否嵌套执行,也就是说开启了事务之后,如果没有执行(exec)或取消(discard)事务,是不能再次开启(multi)事务的,如下图所示。

可以看到,在连续两次输入 multi 命令后,提示 "MULTI calls can not be nested",这个错误提示和源码中的提示是相同的。

multiCommand 函数中在检查后发现事务没有嵌套执行时,则会开启事务,开启事务只是对 redisClient 结构体的 flags 属性设置了 REDIS_MULTI 标志位位运算的 | 通常对标志位进行置位,位运算的 & 通常对标志位进行复位 或 标志位位检测。

discard 命令

discard 命令是用来取消事务的,当使用 multi 开启一个事务后,可以选择使用 exec 命令来以原子的方式执行事务命令队列中的所有命令,也可以使用 discard 命令来取消整个当前的事务。

discard 命令的源码如下。

代码语言:javascript
代码运行次数:0
复制
/**
 * discard命令对应的源码
 */void discardCommand(redisClient *c) {    // 未开启事务
    if(!(c->flags & REDIS_MULTI)) {
        addReplyError(c,"DISCARD without MULTI");
        return;
    }
    
    discardTransaction(c);
    addReply(c,shared.ok);}

discardCommand 函数中也基本上完成了两个功能,第一个功能是判断是否开启了事务第二个功能是取消事务判断事务是否开启,只要检测 redisClient 的属性 flags 即可。取消事务的源码如下。

代码语言:javascript
代码运行次数:0
复制
/**
 * 取消事务
 */void discardTransaction(redisClient *c) {    // 释放事务队列中的所有命令
    freeClientMultiState(c);
    // 初始化multiState的
    initClientMultiState(c);
    c->flags &=~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);;
    // 释放watch中所有的key
    unwatchAllKeys(c);}

在取消事务时一共要完成四件事情首先要释放掉所有事务队列中的命令当开启事务后,所有的命令不会马上执行,而是要进入暂存命令的队列,所以在取消事务时,需要将队列中的命令都释放掉。释放事务队列中的命令的代码如下。

代码语言:javascript
代码运行次数:0
复制
/* Release all the resources associated with MULTI/EXEC state */void freeClientMultiState(redisClient *c) {    int j;

    for(j =0; j < c->mstate.count; j++) {
        int i;
        multiCmd *mc = c->mstate.commands+j;

        for(i =0; i < mc->argc; i++)
            decrRefCount(mc->argv[i]);
        zfree(mc->argv);
    }
    zfree(c->mstate.commands);}

在释放了事务队列中的命令后,接着将 multiState 进行初始化。初始化的代码如下。

代码语言:javascript
代码运行次数:0
复制
/* Client state initialization for MULTI/EXEC */void initClientMultiState(redisClient *c) {    c->mstate.commands = NULL;
    c->mstate.count = 0;}

初始化的过程很简单,mstate 的命令队列置空,队列长度置 0 即可。

初始化后,将 redisClient 的 flags 标志位中关于 事务 和 watch 对应的几个位进行复位。

对于 unwatchAllKeys 函数则是 unwatch 掉所有 watch 的 key,该函数暂时先不看。

redisClient 结构体

在上面的 multiCommand、discardCommand 等几个函数中,它们的参数都是一个 redisClient 结构体指针。它们基本都访问(读写)了两个属性,分别是 flags 和 mstate 。redisClient 结构体想必是一个重要的结构体,它是 Redis 客户端的一个结构体,我们来看一下它的定义,该结构体的定义在 redis.h 文件中。

代码语言:javascript
代码运行次数:0
复制
typedefstruct redisClient {
    ……
    // 标志位
    int flags;              /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
    ……
    // 事务状态
    multiState mstate;      /* MULTI/EXEC state */
    ……
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    ……
} redisClient;

redisClient 结构体的属性特别的多,这里只留下三个与事务相关的属性。分别是 flags、mstate 和 watched_keys 三个属性。第一个是 标志位,第二个是 事务状态,第三个是 watch 链。前两个已经见过了,最后这个暂时先不管。

mstate 是一个 multiState 的结构体,看 multiState 结构体的定义。

代码语言:javascript
代码运行次数:0
复制
/**
 * 事务状态
 */typedef struct multiState {
    // 命令队列
    multiCmd *commands;     /* Array of MULTI commands */
    // 入队命令数量
    int count;              /* Total number of MULTI commands */} multiState;

该结构体的属性有两个,commands 是命令队列,count 表示命令队列的长度。无论是调用命令,还是清空队列,都是根据 count (长度)来逐个的访问 commands。commands 是 multiCmd 的结构体,该结构体的定义如下。

代码语言:javascript
代码运行次数:0
复制
/* Client MULTI/EXEC state */ttypedef struct multiCmd {    // 参数
    robj **argv;
    // 参数个数
    int argc;
    // 命令指针
    struct redisCommand *cmd;} multiCmd;

multiCmd 中保存了命令的参数(argv)、参数的个数(argc),以及命令指针(cmd)。对于 multiState 来说,commands 就是一个 multiCmd 的队列,也就是说,commands 是由多个 multiCmd 组成。

redisClient、multiState 和 multiCmd 三个结构体的大体关系如下图。

exec 命令

exec 命令是用来执行事务队列中命令的。exec 命令对应的源码比较长,我保留了比较重要的代码,其余代码进行了删除,源码如下。

代码语言:javascript
代码运行次数:0
复制
/**
 * exec命令对应的源码
 */ 
void execCommand(redisClient *c) {
    ……
    
    // 判断是否开启事务
    if(!(c->flags & REDIS_MULTI)) {
        addReplyError(c,"EXEC without MULTI");
        return;
    }
    
    ……
    
    // 判断是否有对应的watch,且watch的keys是否被修改
    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;
    }

    // 执行事务前,将所有的wach的keys都unwatch掉
    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);
    for(j =0; j < c->mstate.count; j++) {

        ……
        
        // 执行事务
        call(c,REDIS_CALL_FULL);

        ……
    }
    c->argv = orig_argv;
    c->argc = orig_argc;
    c->cmd = orig_cmd;
    
    ……
}

执行事务时,会判断是否开启了事务,判断是否有 watch 的 keys,且 watch 的 keys 是否有被修改过,如果都没有问题,就将 watch 的 keys 全部 unwatch 掉,最后就开始执行队列中的命令了。

命令入队

最后来看一下开启事务后命令是如何进入队列的。该部分源码在 redis.c 文件中的 processCommand 函数中,该代码比较长,只保留开启事务后入队的代码,源码如下。

代码语言:javascript
代码运行次数:0
复制
int processCommand(redisClient *c) {
    ……

    /* Exec the command */
    if(c->flags & REDIS_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
    {
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else{
        call(c,REDIS_CALL_FULL);
        if(listLength(server.ready_keys))
            handleClientsBlockedOnLists();
    }
    return REDIS_OK;}

当 redis 开启事务后,输入的命令对应的处理方法不是 execCommand、discardCommand 等,就通过 queueMultiCommand 函数来将命令入队,它的源码在 multi.c 下,代码如下。

代码语言:javascript
代码运行次数:0
复制
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++;}

在代码中,使用 zrealloc 调整了命令队列的空间,也就是为新的命令分配了内存空间大小,以便有足够的空间来保存新入队的命令。然后让 mc 指向了当前队列的尾部(也就是新命令要入队的内存的起始位置),从而让新的命令入队。最后增加命令队列的长度。

总结

关于事务的数据结构和代码,就整理这么多,基本上已经算是把事务的大体结构搞明白了,主要就是开始事务、命令入队、执行/取消事务 的源代码,整个代码的流程不算复杂。最后,还有关于 watch 和 unwatch 的部分没有进行阅读,留着下次再来进行整理。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农UP2U 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档