Redis--源码分析--事务

事务:

命令 功能
MULTI 开启事务;
EXEC 执行事务;
WATCH key [key...] 监视数据库键;当乐观锁用;

一、命令实现:

typedef struct redisClient {
	// ...
    multiState mstate;      /* MULTI/EXEC state */
	// ...
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    // ...
} redisClient;
  • L3:进入队列的命令以数组形式保存;
  • L5:每个客户端记录了自己监视的数据库键,以链表保存;
typedef struct redisDb {
	// ...
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    // ...
} redisDb;
  • L3:保存所有监视关系(key:数据库键,val:client链表)的字典;

1、MUTLI:

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);
}
  • L6:打开事务标识,客户端进入事务状态;

​ 当客户端进入事务状态后,服务器处理命令的操作会有所不同,除 EXEC、DISCARD、WATCH、MULTI以外的所有命令都会被放入队列中(L8);

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);
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnLists();
    }
    // ...
}
  • L8:命令入队;
/* 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++;
}

2、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.
     * 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. */
    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++) {
        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命令没有进入命令队列,保证AOF或从服务器获得完整的事务命令*/
        if (!must_propagate && !(c->cmd->flags & REDIS_CMD_READONLY)) {
            execCommandPropagateMulti(c);
            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. */
    /* 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:如果c被标记为REDIS_DIRTY_CAS,那么在EXEC执行时,命令会被拒绝执行;
  • L42:命令传播[2];
  • L48:执行命令[1];

3、WATCH:

​ WATCH命令会将参数中的数据库键插入client维护的链表以及client使用的db维护的字典中;主要讲一下监视事件的触发:

所有回对数据库状态(key space)进行修改的命令,都会调用signalModifiedKey来判断被监视的key是否发生修改;

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

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 */
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
        redisClient *c = listNodeValue(ln);

        c->flags |= REDIS_DIRTY_CAS;
    }
}
  • L20:如果c被标记为REDIS_DIRTY_CAS,那么在EXEC执行时,命令会被拒绝执行;

二、ACID性质:

1、原子性:

​ 事务的原子性指的是:数据库将事务中的多个操作当作一个整体来执行,服务器要么执行事务中的所有操作,要么就一个操作也不执行;

​ Redis通过对命令语法进行检查后

​ 服务器判断命令有错后:就在 processCommand函数中,调用 flagTransaction函数,标记 REDIS_DIRTY_EXEC这个标记会让EXEC命令拒绝执行,从而实现原子性。

void flagTransaction(redisClient *c) {
    if (c->flags & REDIS_MULTI)
        c->flags |= REDIS_DIRTY_EXEC;
}

2、一致性:

​ 事务的一致性指的是:如果数据库在执行事务前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该是一致的。”一致“指的是数据符合数据库本身的定义和要求,没有包含非法或无效的错误数据。

​ 如上文所述,Redis执行命令前都会对命令语法进行检查,所以满足一致性;

3、隔离性:

​ 事物的隔离性指的是,即使数据库中有多个事务并发的执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。

​ 由于当前Reid(3.0)是单线程执行事件循环的,所有请求都会在IO多路复用中串行执行,所以天然具有隔离性;

4、耐久性:

​ 事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果以及被保存到永久性存储介质(磁盘等),即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。

​ Redis事务的耐久性由Redis所使用的持久化模式决定:

  • 当持久化不开启时,Redis事务不具有耐久性;

  • RDB持久化时,除非在每个事务EXEC前都添加一个SAVE命令,否则也不具有耐久性;

  • AOF持久化:

    AOF持久化有2个选项[4]:

    • ALWAYS:该选项会在每个命令执行后进行fdatasync,数据会第一时间保存到磁盘,所有具有耐久性;
    • EVERYSEC:该选项会让程序每秒在后台刷新一次,如果出现停机,可能会丢失长达1s内的数据,所以不具有耐久性;

参考:

  1. Redis源码分析--服务器(2)执行命令的过程 - macguz - 博客园 (cnblogs.com)

  2. Redis源码分析--命令传播 - macguz - 博客园 (cnblogs.com)

  3. cap与一致性(强一致性、弱一致性、最终一致性)_码农和金融的专栏

  4. Redis源码分析--AOF持久化 - macguz - 博客园 (cnblogs.com)

posted @ 2022-02-09 17:47  macguz  阅读(38)  评论(0编辑  收藏  举报