[redis principle] redis transaction source code analysis

I. Introduction

The previous article summarized the use of redis transactions, and today I will share the principles of redis;

2. Concept

redis支持事务机制, but the transaction mechanism of redis is different from the transaction mechanism of traditional relational database.

The essence of redis transaction is a set of commands 集合(命令队列). A transaction can execute multiple commands at once and provides the following guarantees :
1) All commands in the transaction are executed in order. During the execution of transaction commands, command requests submitted by other clients need to wait for all commands in the current transaction to be executed before being processed, and will not be inserted into the current transaction command queue; 2) Either all commands in the transaction will be executed, or none of them will be executed
. Even if some commands in the transaction fail to execute, subsequent commands are still executed. Therefore redis transactions are also atomic.

Note: Redis does not support rollback. If the execution of a command in the transaction fails, Redis will continue to execute subsequent commands instead of rolling back.

3. Redis transaction command

watch命令You can monitor specified keys, and when it is found that these keys have been repaired before subsequent transaction execution, the transaction will be refused to be executed; you
multi命令can open a transaction, and subsequent commands will be placed in the transaction command queue;
exec命令you can execute all commands in the transaction command queue
discard命令and discard the transaction The commands in the command queue and exec命令will end the current transaction;

4. Execution example

1) During transaction execution, when there is no other client executing

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"

You can see that the execution result of exec is:(integer)2

2) During the execution of the transaction, other clients modify the value of the corresponding key.

Insert image description here
Client client2 executes set score 10 after client client1 is executed incr score命令. You can see that the exec execution result is false, indicating client1’s transaction取消执行

5. Implementation Principles of Transactions

1. server.sh/multiStateThe structure is responsible for storing transaction information:

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: Transaction command queue, which stores all commands of the current transaction.
The client attribute client.mstatepoints to a multiState变量multiState that serves as the client's transaction context and is responsible for storing the client's current transaction information.

2. watch command

The dictionary attribute watched_keys is defined in redisDb. The key of the dictionary is the monitored redis key in the database. The value of the dictionary is the list of all clients that monitor the dictionary key. It is responsible for processing. This function will call the processing
watchCommand函数related watch命令logic 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);
}

Every time data is modified in redis, signalModifiedKey函数it will be called to mark the data as modified;
signalModifiedKey函数it will be called to touchWatchedKey函数notify the client monitoring the key that the data has been modified:

/* "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. Implementation of multi and exec commands

multi命令By multiCommand函数processing, the processing of this function is very simple, which is to open the client client_multi标志, which means that the client has been opened 开启事务;
if the transaction has been opened, queueMultiCommand函数it will be called to add the command request to the client transaction command queue 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;
}

The exec command is execCommandhandled by the function:

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 transactions are very simple, that is, executing multiple commands within an atomic operation. The Lua script of redis is also transactional, so users can also use Lua scripts to implement transactions.

6. Summary

  • Redis transactions ensure that multiple commands are executed within one atomic operation;
  • Redis provides multi, exec, discard and watch commands to implement transaction functions;
  • The optimistic locking mechanism can be implemented using the watch command;

Guess you like

Origin blog.csdn.net/weixin_37598243/article/details/128228651