Redis设计与实现 笔记 第十九章 事务

事务

Redis 通过MULTI EXEC WATCH 等命令来实现事务功能.
通过输入 MULTI 开启事务模式,然后输入命令链,最后执行.

19.1 事务的实现

一个事务从开始到结束通常经历一下三个阶段.
1): 事务开始.
2): 命令入队.
3): 事务执行.

19.1.1 事务开始

通过 MULTI 命令来表示事务开始

void multiCommand(redisClient *c) {

    // 不能在事务中嵌套事务
    if (c->flags & REDIS_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");
        return;
    }

    // 打开事务 FLAG
    c->flags |= REDIS_MULTI;

    addReply(c,shared.ok);
}
19.1.2 命令入队

从命令的执行我们可知,当处于非事务状态 命令输入之后会立马进行反馈.

当客户端切换到事务状态时,在非 EXEC,DISCARD,WATCH,MULTI 命令,会将命令放入队列中,直到遇到上面几个命令.

/* Exec the command */
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);

    c->woff = server.master_repl_offset;
    // 处理那些解除了阻塞的键
    if (listLength(server.ready_keys))
        handleClientsBlockedOnLists();
}


/* Add a new command into the MULTI commands queue 
 *
 * 将一个新命令添加到事务队列中
 */
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++;
}

19.1.3 事务队列

通过FIFO 的队列来保存当前事务的待执行命令.

struct redisClinet {
    multiState mstate;
};

/*
 * 事务命令
 */
typedef struct multiCmd {

    // 参数
    robj **argv;

    // 参数数量
    int argc;

    // 命令指针
    struct redisCommand *cmd;

} multiCmd;
19.1.4 执行事务

当一个处于事务状态的客户端想服务器发送 EXEC 命令时,将会被立即执行,包括在事务队列中的待执行命令. 并将所有的结果返回至客户端.

void execCommand(redisClient *c) {
    int j;
    robj **orig_argv;
    int orig_argc;
    struct redisCommand *orig_cmd;
    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.
     *    命令在入队时发生错误
     *    (注意这个行为是 2.6.4 以后才修改的,之前是静默处理入队出错命令)
     *
     * 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. 
     *
     * 第一种情况返回多个批量回复的空对象
     * 而第二种情况则返回一个 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 */
    // 已经可以保证安全性了,取消客户端对所有键的监视
    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++) {

        // 因为 Redis 的命令必须在客户端的上下文中执行
        // 所以要将事务队列中的命令、命令参数等设置给客户端
        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.
         *
         * 当遇上第一个写命令时,传播 MULTI 命令。
         *
         * 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. 
         *
         * 这可以确保服务器和 AOF 文件以及附属节点的数据一致性。
         */
        if (!must_propagate && !(c->cmd->flags & REDIS_CMD_READONLY)) {

            // 传播 MULTI 命令
            execCommandPropagateMulti(c);

            // 计数器,只发送一次
            must_propagate = 1;
        }

        // 执行命令
        call(c,REDIS_CALL_FULL);

        /* Commands may alter argc/argv, restore mstate. */
        // 因为执行后命令、命令参数可能会被改变
        // 比如 SPOP 会被改写为 SREM
        // 所以这里需要更新事务队列中的命令和参数
        // 确保附属节点和 AOF 的数据一致性
        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. */
    // 将服务器设为脏,确保 EXEC 命令也会被传播
    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);
}

19.2 WATCH 命令的实现

从Redis 的事务执行,我们不难发现这样的问题, 由于Redis 支持多客户端模式,所以说一个客户端在进入事务模式后,执行了一连串的命令,但另一个客户端对事务命令队列中的键执行了修改,那么对于之前开启事务的客户端来说,该任务队列的执行结果可能不符合自己的预期值.这个时候就要借助额外的规则进行一个约束.这就是 WATCH 命令的作用

WATCH 命令是回一个乐观锁,可以在 EXEC 命令执行之前,监视任意数量的键,在进行 EXEC 判断当前监视的键的对象是否发生了改变.

在字典中的键值对, 键由监视的字符键组成, 值则是一个链表, 表示当前键有那些客户端在 监视

struct redisDb{
  
    //正在被 WATCH 命令监视的键  
    dict* watched_keys;  
};
19.2.2 监视机制的触发

在进行修改命令时, 这些命令会执行

/*-----------------------------------------------------------------------------
 * Hooks for key space changes.
 *
 * 键空间改动的钩子。
 *
 * Every time a key in the database is modified the function
 * signalModifiedKey() is called.
 *
 * 每当数据库中的键被改动时, signalModifiedKey() 函数都会被调用。
 *
 * Every time a DB is flushed the function signalFlushDb() is called.
 *
 * 每当一个数据库被清空时, signalFlushDb() 都会被调用。
 *----------------------------------------------------------------------------*/

void signalModifiedKey(redisDb *db, robj *key) {
    touchWatchedKey(db,key);
}

/* "Touch" a key, so that if this key is being WATCHed by some client the
 * next EXEC will fail. 
 *
 * “触碰”一个键,如果这个键正在被某个/某些客户端监视着,
 * 那么这个/这些客户端在执行 EXEC 时事务将失败。
 */
void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;

    // 字典为空,没有任何键被监视
    if (dictSize(db->watched_keys) == 0) return;

    // 获取所有监视这个键的客户端
    clients = dictFetchValue(db->watched_keys, key);
    if (!clients) return;

    /* Mark all the clients watching this key as REDIS_DIRTY_CAS */
    /* Check if we are already watching for this key */
    // 遍历所有客户端,打开他们的 REDIS_DIRTY_CAS 标识
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
        redisClient *c = listNodeValue(ln);

        c->flags |= REDIS_DIRTY_CAS;
    }
}
19.2.3 判断事务是否安全

可以在上一小节看到,当被监视的键被进行修改之后,该客户端的标记 flags 会打上 REDIS_DIRTY_CAS 标记.
所以,我应当想到,在执行 EXEC 命令时, Redis 会对 REDIS_DIRTY_CAS 标记进行判断,从而判断监视的键是否进行过更改

/* Check if we need to abort the EXEC because:
 *
 * 检查是否需要阻止事务执行,因为:
 *
 * 1) Some WATCHed key was touched.
 *    有被监视的键已经被修改了
 *
 * 2) There was a previous error while queueing commands.
 *    命令在入队时发生错误
 *    (注意这个行为是 2.6.4 以后才修改的,之前是静默处理入队出错命令)
 *
 * 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. 
 *
 * 第一种情况返回多个批量回复的空对象
 * 而第二种情况则返回一个 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;
}
19.2.4 一个完成的 WATCH 事务执行过程

客户端A:

首先是对一个键进行监视

WATCH "name"

然后发起一个事务命令

MULTI

然后客户端A发起命令队列.

客户端B进行修改

SET "name" "123"

此时 客户端A输入

EXEC

事务执行失败.

19.3 事务的 ACID 性质

在 Redis 中, 事务总是具有原子性(Atomicity),一致性(Consistency)和隔离性(Isolation).并且当 Redsi 处于一种持久化模式下生,事务也具有耐久性.

19.3.1 原子性

原子性表示,一个事务要么全部执行,要么一个不执行.将命令队列中的所有指令,都当成一个命令来执行.

但是 Redis的事务和传统的关系数据库事务的最大区别在于, Redis 不支持事务回滚机制.即使某个命令在执行期间出现了错误,整个事务也会继续执行下去.

Redis 的坐着在事务功能的文档中解释,不支持事务回滚是因为这种复杂的功能和 Redis 追求简单高效的设计主旨不符.并且认为 Redis 事务的执行时错误通常是编程错误产生的,这种错误通常只会出现在开发环境中,很少在实际生产环境中出现.所以没有必要为 Redis 开发事务回滚功能

19.3.2 一致性

一致指的是数据符合数据库本身的定义和要求.没有包含非法或者无效的错误数据.
书中通过介绍三个事务可能出错的地方,并说明 Redis 是如何妥善的处理.

1: 入队错误

当收到错误命令在事务命令队列中时,事务将拒绝进行执行,所以不会收到影响

2: 执行错误

执行错误在遇到入队无法检测的错误时,会在实际执行时被触发,当然即使在执行中发生了错误,服务器也不会中断事务的执行,会继续执行事务中余下的其他命令.
该错误会在事务执行中直接被发现,也不会造成影响.

针对这个错误,我个人理解为,在逻辑的执行上,可能我们无法得到我们期望的结果,毕竟一个命令发生了错误,但是,对数据库而言,并没有什么问题,我完整的执行了你的命令,这个错误我也进行了响应.

3: 服务器停机

在事务的执行过程中发生了停机时是否能保证一致性呢.
分为两种情况:
服务器没开启持久化模式.那么重启之后服务器就是空的.所以服务器永远是一致的.

如果服务器开启了持久化模式,如果是 RDB 模式,那么也没有进行保存,如果在过程中发生宕机,将回到 RDB 文件的当时状态,那么也是一致的.
如果开启了 AOF 模式,那么在文件恢复过程中, AOF 会依次执行 AOF 文件中的命令, 也就会让事务重新运行一遍.当然也不会导致一致性出现问题.

19.3.3 隔离性

Redis 是单线程,事务是一个一个运行的,并不会产生干扰

19.3.4 耐久性

持久性是指,执行结果是否能被保存到永久性存储介质里面.即使完成后关机,执行的结果也不会丢失

当服务器在无持久化的内存模式下,事务不具有持久性,一旦停机就丢失了.
如果在RDB 持久化模式下,只有在特定的条件满足下才会进行存盘,因此也不具有耐久性

当服务器在 AOF 持久化模式下,并且 appendfsync 值为 always 时,每一条数据都会执行同步函数,进行存盘,所以有持久性.
当 appendfsync 为 everysec 时,程序会每秒同步一次命令数据到硬盘,所以可能会造成丢失,不具有持久性.
当 appendfsync 为 no 时,只有当主动发出 SAVE 命令时,才会进行存盘,所以也不具有持久性.

书中给出了一个方法,在每次事务完成后进行手动的命令存盘, SAVE 但是效率太低,但是我们可以在AOF 持久化模式下,对关键的事务进行主动存盘.

总结

事务就是一个将多种命令打包,一次性有序的执行的机制.
多个命令会被入队到失误队列中,按先进先出的顺序执行.
事务在执行过程中不会被中断.
WATCH 命令作为乐观锁,当进行监视时,被其他客户端进行更改,监视客户端的 REDIS_DIRTY_CAS 标志会被添加.
只有在客户端 REDIS_DIRTY_CAS 未被打开的时,服务器才会只进行对应的命令队列.
Rdis 的事务具有 ACID 中的原子性,一致性和隔离性, 当服务器运行在 AOF 持久化模式下,并且同步选项 appednfsync 为 always 时,事务也具有耐久性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值