【redis原理】redis事务源码分析

一、前言

上一篇文章总结了redis事务的使用,今天分享redis的原理;

二、概念

redis支持事务机制,但是redis的事务机制与传统关系型数据库的事务机制并不相同。

redis事务的本质是一组命令的集合(命令队列)。事务可以一次执行多个命令,并提供以下保证
1)事务中的所有命令都按顺序执行。事务命令执行过程中,其他客户端提交的命令请求需要等待当前事务所有命令执行完成后再处理,不会插入当前事务命令队列中;
2)事务中的命令要么都执行,要么都不执行,即使事务中有些命令执行失败,后续命令依然被执行。因此redis事务也是原子的。

注意:redis不支持回滚,如果事务中有命令执行失败了,那么redis会继续执行后续命令而不是回滚。

三、redis事务命令

watch命令可以监听指定键,当后续事务执行前发现这些键已修复时,则拒绝执行事务;
multi命令可以开启一个事务,后续的命令都会被放入事务命令队列;
exec命令可以执行事务命令队列中的所有命令
discard命令可以抛弃事务命令队列中的命令,和exec命令都会结束当前事务;

四、执行示例

1)在事务执行过程中,没有其它客户端执行时

127.0.0.1:6379> set score 1
OK
127.0.0.1:6379> 
127.0.0.1:6379> 
127.0.0.1:6379> watch score
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr score
QUEUED
127.0.0.1:6379> exec
1) (integer) 2
127.0.0.1:6379> get score
"2"

可以看到exec的执行结果为:(integer)2

2)在事务执行过程中,有其它客户端修改对应key的值

在这里插入图片描述
客户端client2在客户端client1执行完incr score命令之后执行set score 10,可以看到exec执行结果为false,说明client1的事务取消执行

五、事务的实现原理

1、server.sh/multiState结构体负责存放事务信息:

typedef struct multiState {
    
    
    multiCmd *commands;     /* Array of MULTI commands */
    int count;              /* Total number of MULTI commands */
    int cmd_flags;          /* The accumulated command flags OR-ed together.
                               So if at least a command has a given flag, it
                               will be set in this field. */
    int cmd_inv_flags;      /* Same as cmd_flags, OR-ing the ~flags. so that it
                               is possible to know if all the commands have a
                               certain flag. */
    int minreplicas;        /* MINREPLICAS for synchronous replication */
    time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;

commands:事务命令队列,存放当前事务所有的命令。
客户端属性client.mstate指向一个multiState变量,该multiState作为客户端的事务上下文,负责存放该客户端当前的事务信息。

2、watch命令

redisDb中定义了字典属性watched_keys,该字典的键是数据库中被监视的redis键,字典的值是监视字典键的所有客户端列表;
watchCommand函数负责处理watch命令,该函数会调用watchForKey函数处理相关逻辑:

/* ===================== WATCH (CAS alike for MULTI/EXEC) ===================
 *
 * The implementation uses a per-DB hash table mapping keys to list of clients
 * WATCHing those keys, so that given a key that is going to be modified
 * we can mark all the associated clients as dirty.
 *
 * Also every client contains a list of WATCHed keys so that's possible to
 * un-watch such keys when the client is freed or when UNWATCH is called. */

/* In the client->watched_keys list we need to use watchedKey structures
 * as in order to identify a key in Redis we need both the key name and the
 * DB */
typedef struct watchedKey {
    
    
    robj *key;
    redisDb *db;
} watchedKey;

/* Watch for the specified key */
void watchForKey(client *c, robj *key) {
    
    
    list *clients = NULL;
    listIter li;
    listNode *ln;
    watchedKey *wk;

    /* Check if we are already watching for this key */
    listRewind(c->watched_keys,&li);
    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 */
    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 */
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);
}

redis中每次修改数据时,都会调用signalModifiedKey函数,将该数据标志为已修改;
signalModifiedKey函数会调用touchWatchedKey函数,通知监视该键的客户端数据已修改:

/* "Touch" a key, so that if this key is being WATCHed by some client the
 * next EXEC will fail. */
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 CLIENT_DIRTY_CAS */
    /* Check if we are already watching for this key */
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
    
    
        client *c = listNodeValue(ln);

        c->flags |= CLIENT_DIRTY_CAS;
    }
}

3、multi、exec命令的实现

multi命令multiCommand函数处理,该函数的处理非常简单,就是打开客户端client_multi标志,代表该客户端已开启事务
如果事务已开启,会调用queueMultiCommand函数,将命令请求添加到客户端事务命令队列client.mstate.commands中:

/* Add a new command into the MULTI commands queue */
void queueMultiCommand(client *c) {
    
    
    multiCmd *mc;
    int j;

    /* No sense to waste memory if the transaction is already aborted.
     * this is useful in case client sends these in a pipeline, or doesn't
     * bother to read previous responses and didn't notice the multi was already
     * aborted. */
    if (c->flags & CLIENT_DIRTY_EXEC)
        return;

    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++;
    c->mstate.cmd_flags |= c->cmd->flags;
    c->mstate.cmd_inv_flags |= ~c->cmd->flags;
}
int processCommand(client *c) {
    
    
...
/* Exec the command */
    if (c->flags & CLIENT_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,CMD_CALL_FULL);
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnKeys();
    }
    return C_OK;
}

exec命令由execCommand函数处理:

void execCommand(client *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? */
    int was_master = server.masterhost == NULL;

    if (!(c->flags & CLIENT_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 & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
    
    
        addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :
                                                   shared.nullarray[c->resp]);
        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;
    addReplyArrayLen(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 command which
         * is not readonly nor an administrative one.
         * 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. */
        if (!must_propagate &&
            !server.loading &&
            !(c->cmd->flags & (CMD_READONLY|CMD_ADMIN)))
        {
    
    
            execCommandPropagateMulti(c);
            must_propagate = 1;
        }

        int acl_keypos;
        int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
        if (acl_retval != ACL_OK) {
    
    
            addACLLogEntry(c,acl_retval,acl_keypos,NULL);
            addReplyErrorFormat(c,
                "-NOPERM ACLs rules changed between the moment the "
                "transaction was accumulated and the EXEC call. "
                "This command is no longer allowed for the "
                "following reason: %s",
                (acl_retval == ACL_DENIED_CMD) ?
                "no permission to execute the command or subcommand" :
                "no permission to touch the specified keys");
        } else {
    
    
            call(c,server.loading ? CMD_CALL_NONE : CMD_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) {
    
    
        int is_master = server.masterhost == NULL;
        server.dirty++;
        /* If inside the MULTI/EXEC block this instance was suddenly
         * switched from master to slave (using the SLAVEOF command), the
         * initial MULTI was propagated into the replication backlog, but the
         * rest was not. We need to make sure to at least terminate the
         * backlog with the final EXEC. */
        if (server.repl_backlog && was_master && !is_master) {
    
    
            char *execcmd = "*1\r\n$4\r\nEXEC\r\n";
            feedReplicationBacklog(execcmd,strlen(execcmd));
        }
    }

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 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的Lua脚本也是事务性的,所以用户也可以使用lua脚本实现事务。

六、总结

  • redis事务保证多条命令在一个原子操作内执行;
  • redis提供了multi、exec、discard和watch命令来实现事务功能;
  • 使用watch命令可以实现乐观锁机制;

猜你喜欢

转载自blog.csdn.net/weixin_37598243/article/details/128228651