redis数据库之事务

本文主要结合源码探讨Redis事务的实现。通过介绍结构体与命令处理流程,阐述了客户端与服务器如何通过命令进行通信,以及事务如何从队列中取出并执行。对于DISCARD和WATCH等其他事务相关命令,建议读者自行研究。
摘要由CSDN通过智能技术生成

redis事务的原理介绍在redis设计与实现这本书中已经讲述的非常清楚,这里就不浪费更多的时间去讲述了。这篇博客的目的主要还是结合源码来讲述redis事务的实现。如果大家对redis有点熟悉,都知道redis客户端和服务器之间的通信都是通过命令+key+值来进行通信的(内部协议的格式在以后的博客中讲述)。redis的事务也是通过命令来实现的。

MULTI    事务的开始
EXEC      按顺序执行事务中的命令
DISCARD   取消事务
这三个命令实现事务的基本功能。除了这三个命令还有个WATCH,WATCH命令的作用在redis设计与实现中也有详细的讲述,这里也就不浪费口舌了。

在正式讲述事务之前,先来介绍两个结构体:

typedef struct multiCmd {
    // 执行命令的所有 key 对象
    robj **argv;
    // 参数的数量
    int argc;
    // 被执行的命令
    struct redisCommand *cmd;
} multiCmd;

/*
 * 事务状态结构
 */
typedef struct multiState {
    // 数组,保存着所有在事务队列中的命令
    multiCmd *commands;     /* Array of MULTI commands */
    // 队列中命令的数量
    int count;              /* Total number of MULTI commands */
} multiState;
上面两个结构体就是关于事务的信息。上面都有足够多的解释,这里也就不一一讲述了。现在让我们来从事务开始来讲起,也就是MULTI对应的函数:multiCommand

void multiCommand(redisClient *c) {
    // MULTI 命令不能嵌套
    if (c->flags & REDIS_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");
        return;
    }
    
    // 打开事务的 FLAG
    // 从此之后,除 DISCARD 和 EXEC 等少数几个命令之外
    // 其他所有的命令都会被添加到事务队列里
    c->flags |= REDIS_MULTI;
    addReply(c,shared.ok);
}
从上面的源码来看,这个函数比较简单。最主要还是下面这句话:

c->flags |= REDIS_MULTI;
c是一个redisClient类型的数据,redisClient就是对应着当前操作数据库的客户端的信息。所以把当前客户端设置为REDIS_MULTI也就说明当前客户端处于事务状态。然后就立马发送OK给客户端。那具体插入命令到事务中是如何操作的呢?来看下processCommand这个函数的实现:

int processCommand(redisClient *c) {
.....
    if (c->flags & REDIS_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
    {
        // 如果正在执行事务,
        // 并且新命令不是 EXEC / DISCARD / MULTI / WATCH 
        // 那么将它们追加到事务队列
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else {
        // 执行命令
        call(c,REDIS_CALL_FULL);

        // 每次执行完命令之后,处理所有就绪列表
        if (listLength(server.ready_keys))
            handleClientsBlockedOnLists();
    }
.....
}
processCommand的一些预处理就直接省略了,看上面的代码,如果当前的客户端处于事务状态并且命令不是MULTI,DISCARD,EXEC,WATCH都会把当前的命令插入到队列中,并且发送QUEUED给客户端,否则客户端不处于事务状态,那就是redis正常的处理流程。下面来看下queueMultiCommand

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++;
}
这个函数也比较简单,主要就是重新分配空间,把当前命令及其参数入队列。处于事务状态的客户端发送的命令入队列这里讲清楚了,那客户端执行EXEC的时候,redis服务器是怎么操作的呢?当redis服务器收到EXEC命令后也会经过processCommand,上面已经讲述过如果是EXEC,MULTI,DISCARD,WATCH这四种命令时,是不会入队列的,而是立马执行。与EXEC对应的函数是execCommand,下面来看下这个函数:

void execCommand(redisClient *c) {
......
    if (!(c->flags & REDIS_MULTI)) {
        addReplyError(c,"EXEC without MULTI");
        return;
    }
......
    if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
        // 根据状态,决定返回的错误的类型
        addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
                                                  shared.nullmultibulk);

        // 以下四句可以用 discardTransaction() 来替换
        freeClientMultiState(c);
        initClientMultiState(c);
        c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);
        unwatchAllKeys(c);

        goto handle_monitor;
    }
......
for (j = 0; j < c->mstate.count; j++) {
        // 因为 call 可能修改命令,而命令需要传送给其他同步节点
        // 所以这里将要执行的命令(及其参数)先备份起来
        c->argc = c->mstate.commands[j].argc;
        c->argv = c->mstate.commands[j].argv;
        c->cmd = c->mstate.commands[j].cmd;

        // 执行命令
        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;
    }
......
}
函数刚开始就会判断客户端是不是处于事务状态,如果不在事务状态是不能执行EXEC这个命令的。 还有就是要注意,如果当一个客户端处于事务状态,并且操作了一些key,但是这个key被另外一个客户端修改,并且前一个客户端还处于事务状态,这时事务状态的客户端会执行事务失败。

下面的for循环就是把命令从队列中取出再一个个执行,这里的逻辑也比较简单也就不详述了。

自此,redis事务的开始以及事务命令入队列以及取出命令执行都讲完了,至于DISCARD和WATCH就让大家自己去看吧。









评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值