结合redis设计与实现的redis源码学习-16-事务(multi.c)

Redis通过MULTI,EXEC,WATCH等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性,按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求。
在client结构体中用mstate来表示事务的状态,它是一个结构体:

//事务命令的参数
typedef struct multiCmd {
    robj **argv;//参数对象指针数组
    int argc;//参数个数
    struct redisCommand *cmd;//命令指针
} multiCmd;

typedef struct multiState {
    multiCmd *commands;     /* Array of MULTI commands 事务命令的数组*/
    int count;              /* Total number of MULTI commands 事务命令个数*/
    int minreplicas;        /* MINREPLICAS for synchronous replication 用于同步复制的*/
    time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. 同福复制的超时时间*/
} multiState;
事务队列

事务队列使用multiCmd类型的数组表示,每个multiCmd结构都保存了一个已入队命令的相关信息;
事务队列以先进先出的方式保存入队的命令,较先入队的命令被放到数组的前面。

事务执行

当处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令的结果全部返回给客户端。

WATCH命令

它是一个乐观锁,他可以在EXEC命令执行前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表食物执行失败的空回复。
每个Redis数据库都保存这个一个watchkey的字典,这个字典的键是某个被watch监视的数据库键,而字典的值则是一个链表,记录了所有监视相应数据库键的客户端;
所有对数据库进行修改的命令,在执行之后都会调用touchWatchKey函数对字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有,那么会将监视被修改键的客户端的dirty标识打开,表示该客户端的事务安全性已经被破坏。当服务器接收客户端的EXEC命令时,会根据这个标识来判断是否执行事务。

Redis事务的ACID性质

原子性(atomicity):要么都执行,要么一个都不执行
一致性:数据总是一致的
隔离性:串行方式
耐久性:AOF

看代码

#include "server.h"
/* ================================ MULTI/EXEC ============================== */

/* Client state initialization for MULTI/EXEC 初始化事务状态*/
void initClientMultiState(client *c) {
    c->mstate.commands = NULL;
    c->mstate.count = 0;
}
/* Release all the resources associated with MULTI/EXEC state 释放客户端事务状态*/
void freeClientMultiState(client *c) {
    int j;

    for (j = 0; j < c->mstate.count; j++) {//遍历所有命令
        int i;
        multiCmd *mc = c->mstate.commands+j;

        for (i = 0; i < mc->argc; i++)
            decrRefCount(mc->argv[i]);//遍历所有参数
        zfree(mc->argv);
    }
    zfree(c->mstate.commands);
}
/* Add a new command into the MULTI commands queue 将一个新命令插入事务队列*/
void queueMultiCommand(client *c) {
    multiCmd *mc;
    int j;

    c->mstate.commands = zrealloc(c->mstate.commands,
            sizeof(multiCmd)*(c->mstate.count+1));//扩大命令状态,这里每次插入都会realloc,效率?
    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++;
}
//丢弃事务
void discardTransaction(client *c) {
    freeClientMultiState(c);//释放事务状态
    initClientMultiState(c);//初始化事务状态
    c->flags &= ~(CLIENT_MULTI|CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC);//将flags置为与事务无关
    unwatchAllKeys(c);//取消客户端所有watch的键
}


/* Flag the transacation as DIRTY_EXEC so that EXEC will fail. Should be called every time there is an error while queueing a command. 事务标记为DIRTY_EXEC,以使EXEC失败,每次排队命令时都应该调用*/
void flagTransaction(client *c) {
    if (c->flags & CLIENT_MULTI)
        c->flags |= CLIENT_DIRTY_EXEC;
}
//执行multi命令
void multiCommand(client *c) {
    if (c->flags & CLIENT_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");//如果已经使用事务了,不能嵌套使用
        return;
    }
    c->flags |= CLIENT_MULTI;
    addReply(c,shared.ok);//返回成功状态
}
//撤销命令
void discardCommand(client *c) {
    if (!(c->flags & CLIENT_MULTI)) {
        addReplyError(c,"DISCARD without MULTI");//没有处在事务状态
        return;
    }
    discardTransaction(c);//撤销事务
    addReply(c,shared.ok);
}

/* Send a MULTI command to all the slaves and AOF file. Check the execCommand implementation for more information. 发送事务命令到所有的从属和AOF文件,检查执行命令的执行过程*/
void execCommandPropagateMulti(client *c) {
    robj *multistring = createStringObject("MULTI",5);

    propagate(server.multiCommand,c->db->id,&multistring,1,
              PROPAGATE_AOF|PROPAGATE_REPL);//传播命令
    decrRefCount(multistring);
}
//执行事务
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? 是否需要将命令发送给从属和AOF的flag*/

    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.//一些watch的键不给改变了
     * 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. 在第一种情况下失败返回一个多批量nil对象,第二个会返回EXECABORT错误*/
    if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
        addReply(c, c->flags & CLIENT_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 尽快解决,否则我们会浪费CPU周期*/
    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/exec作为一个整体,aof和复制链接将具有相同的一致性和原子性保证 */
        if (!must_propagate && !(c->cmd->flags & CMD_READONLY)) {
            execCommandPropagateMulti(c);
            must_propagate = 1;
        }

        call(c,CMD_CALL_FULL);//调用命令

        /* Commands may alter argc/argv, restore mstate. 命令可能会改变参数和参数个数,恢复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. 在multi命令传播后确保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 CMD_SKIP_MONITOR in the command table, and we do it here with correct ordering. 将EXEC发送给等待MONITOR数据的客户端,我们在这里执行他,因为命令执行的自然瞬狙实际上是MUTLI,EXEC,内部命令,,,相反,EXEC在命令表中被标记为CMD_SKIP_MONITOR,我们在这里以正确的顺序执行*/
    if (listLength(server.monitors) && !server.loading)
        replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}

下面是watch的实现方式

/* 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 在客户端的watchkey表中,我们需要使用watchedKey结构,以便在Redis中标识一个秘钥,我们需要密要名称和数据库*/
typedef struct watchedKey {
    robj *key;
    redisDb *db;
} watchedKey;

/* Watch for the specified key 观察指定的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 检查是否已经watch这个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 在这个数据库中找到了这个key*/
    }
    /* This key is not already watched in this DB. Let's add it 没有找到,添加*/
    clients = dictFetchValue(c->db->watched_keys,key);//在watch字典中找这个键
    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 将新键添加到这个客户端watch的链表*/
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);
}

/* Unwatch all the keys watched by this client. To clean the EXEC dirty flag is up to the caller. 取消这个客户端watch的所有键,清空dirty标识*/
void unwatchAllKeys(client *c) {
    listIter li;
    listNode *ln;

    if (listLength(c->watched_keys) == 0) return;
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {//表里watchkey的表
        list *clients;
        watchedKey *wk;

        /* Lookup the watched key -> clients list and remove the client from the list 查找watchkey,并且从列表中删除这个客户端*/
        wk = listNodeValue(ln);
        clients = dictFetchValue(wk->db->watched_keys, wk->key);
        serverAssertWithInfo(c,NULL,clients != NULL);
        listDelNode(clients,listSearchKey(clients,c));
        /* Kill the entry at all if this was the only client 如果只有这个客户端watch,就删除掉键*/
        if (listLength(clients) == 0)
            dictDelete(wk->db->watched_keys, wk->key);
        /* Remove this watched key from the client->watched list */
        listDelNode(c->watched_keys,ln);
        decrRefCount(wk->key);
        zfree(wk);
    }
}

/* "Touch" a key, so that if this key is being WATCHed by some client the next EXEC will fail. 触摸一个key,如果有客户端watch它,那么该客户端的下一次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 CLIENT_DIRTY_CAS 将所有watch这个键的客户端标志打开*/
    /* 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;
    }
}

/* On FLUSHDB or FLUSHALL all the watched keys that are present before the flush but will be deleted as effect of the flushing operation should be touched. "dbid" is the DB that's getting the flush. -1 if it is a FLUSHALL operation (all the DBs flushed). 在重置数据库上,所有在冲洗前存在的键都会被touch,但会被删除。dbid是获取刷新的数据库*/
void touchWatchedKeysOnFlush(int dbid) {
    listIter li1, li2;
    listNode *ln;

    /* For every client, check all the waited keys 检查所有客户端等待的键*/
    listRewind(server.clients,&li1);
    while((ln = listNext(&li1))) {
        client *c = listNodeValue(ln);//遍历每个客户端
        listRewind(c->watched_keys,&li2);
        while((ln = listNext(&li2))) {//遍历每个客户端watch的key
            watchedKey *wk = listNodeValue(ln);

            /* For every watched key matching the specified DB, if the key exists, mark the client as dirty, as the key will beremoved. 对于与指定数据库匹配的每个观看秘钥,如果秘钥存在,则将该客户端标记为脏,因为改密要将被删除*/
            if (dbid == -1 || wk->db->id == dbid) {
                if (dictFind(wk->db->dict, wk->key->ptr) != NULL)
                    c->flags |= CLIENT_DIRTY_CAS;
            }
        }
    }
}
//watch命令执行
void watchCommand(client *c) {
    int j;

    if (c->flags & CLIENT_MULTI) {
        addReplyError(c,"WATCH inside MULTI is not allowed");
        return;
    }
    for (j = 1; j < c->argc; j++)
        watchForKey(c,c->argv[j]);
    addReply(c,shared.ok);
}
//取消watch
void unwatchCommand(client *c) {
    unwatchAllKeys(c);
    c->flags &= (~CLIENT_DIRTY_CAS);
    addReply(c,shared.ok);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值