Redis源码分析(十六)——事务操作Multi

来源:互联网 发布:南京程序员工资 编辑:程序博客网 时间:2024/05/13 18:56

事务:即将多个命令打包,然后一次性,按顺序的执行。事务的所有命令在执行完成之前不会被中断,在此期间服务器不接受任何其他的客户端请求。

一个事务的基本操作步骤为: 开始事务——>命令入队——>执行命令。

该过程由四个基本命令完成: MULTI、 DISCARD、EXEC、WATCH。

 

开始事务MULTI:

 将客户端的REDIS_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);}



将一个新的命令添加进事务队列:

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++;}


执行事务EXEC:

注意 MULTI、 DISCARD、EXEC、WATCH这四个命令不会被放入事务队列中,而是直接执行。   当客户端处于事务状态时,执行EXEC命令,服务器将会根据事务队列,以FIFO的方式依次执行里面的命令,并将没条命令的执行结果保存在一个回复队列中,当执行完事务队列中的命令后,ECEX返回回复队列中的结果给客户端,客户端从事务状态切换到非事务状态。  事务执行完毕。


事务执行命令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);}


撤销事务DISCARD:

用于撤销一个事务,清空客户端的事务队列,并将客户端切换到非事务状态。

void discardTransaction(redisClient *c) {    // 重置事务状态    freeClientMultiState(c);    initClientMultiState(c);    // 屏蔽事务状态    c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);;    // 取消对所有键的监视    unwatchAllKeys(c);}


监视WATCH:

在客户端进入事务状态之前监视任意数量的键:当执行事务EXEC时,如果被监视的任意一个键被其他客户端修改,则整个事务不再执行,直接返回。

WATCH命令的实现: 每个数据库有一个监视字典:键为被任客户端监视的key,值为一个客户端链表,保存监视该key的客户端。如下:

  

WATCH的作用就是将当前客户端与要监视的键在监视字典watched_keys中进行关联。   另外: 每个客户端都有一个监视链表:保存了该客户端监视的所有键。

WATCH的触发: 任何修改数据库键空间的命令执行完后都会调用touchWatchkey函数——检查数据库的监视字典,如果有客户端正在监视该被修改的键,则所有监视这个键的客户端的REDIS_DIRTY_CAS选项都会被打开,表示事务安全性被破坏,当执行EXEC命令时,检查REDIS_DIRTY_CAS选项,如果该选项被打开则服务器放弃执行这个事务,直接放客户端返回空回复。


让客户端监视给定的key: 将监视的key添加进数据库的监视字典  并添加到客户端的监视key链表中

void watchForKey(redisClient *c, robj *key) {    list *clients = NULL;    listIter li;    listNode *ln;    watchedKey *wk;    /* Check if we are already watching for this key */    // 检查 key 是否已经保存在 watched_keys 链表中,    // 如果是的话,直接返回    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 */    }    // 键不存在于 watched_keys ,添加它    // 以下是一个 key 不存在于字典的例子:    // before :    // {    //  'key-1' : [c1, c2, c3],    //  'key-2' : [c1, c2],    // }    // after c-10086 WATCH key-1 and key-3:    // {    //  'key-1' : [c1, c2, c3, c-10086],    //  'key-2' : [c1, c2],    //  'key-3' : [c-10086]    // }    /* This key is not already watched in this DB. Let's add it */    // 检查 key 是否存在于数据库的 watched_keys 字典中    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 */    // 将新 watchedKey 结构添加到客户端 watched_keys 链表的表尾    // 以下是一个添加 watchedKey 结构的例子    // before:    // [    //  {    //   'key': 'key-1',    //   'db' : 0    //  }    // ]    // after client watch key-123321 in db 0:    // [    //  {    //   'key': 'key-1',    //   'db' : 0    //  }    //  ,    //  {    //   'key': 'key-123321',    //   'db': 0    //  }    // ]    wk = zmalloc(sizeof(*wk));    wk->key = key;    wk->db = c->db;    incrRefCount(key);    listAddNodeTail(c->watched_keys,wk);}


让客户端监视任意多给定的键:

void watchCommand(redisClient *c) {    int j;    // 不能在事务开始后执行    if (c->flags & REDIS_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);}


取消客户端对所有键的监视。(在数据库监视字典以及客户端的监视链表依次删除所有该客户端监视链表中的key)。清除客户端事务状态的任务由调用者执行:
void unwatchAllKeys(redisClient *c) {    listIter li;    listNode *ln;    // 没有键被监视,直接返回    if (listLength(c->watched_keys) == 0) return;    // 遍历链表中所有被客户端监视的键    listRewind(c->watched_keys,&li);    while((ln = listNext(&li))) {        list *clients;        watchedKey *wk;        /* Lookup the watched key -> clients list and remove the client         * from the list */        // 从数据库的 watched_keys 字典的 key 键中        // 删除链表里包含的客户端节点        wk = listNodeValue(ln);        // 取出客户端链表        clients = dictFetchValue(wk->db->watched_keys, wk->key);        redisAssertWithInfo(c,NULL,clients != NULL);        // 删除链表中的客户端节点        listDelNode(clients,listSearchKey(clients,c));        /* Kill the entry at all if this was the only client */        // 如果链表已经被清空,那么删除这个键        if (listLength(clients) == 0)            dictDelete(wk->db->watched_keys, wk->key);        /* Remove this watched key from the client->watched list */        // 从链表中移除 key 节点        listDelNode(c->watched_keys,ln);        decrRefCount(wk->key);        zfree(wk);    }}




0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 麦芒5电池不耐用怎么办 华为7x照相模糊怎么办 华为麦芒6照相虚怎么办 荣耀8gps信号弱怎么办 华为麦芒4手机卡顿怎么办 华为麦芒4玩游戏卡怎么办 sim卡换卡通讯录丢了怎么办 换sim卡通讯录怎么办 麦芒4开不了机怎么办 麦芒5开不了机怎么办 麦芒6针丢了怎么办 麦芒6扬声器坏了怎么办 华为手机2s太卡怎么办 华为麦芒6网速慢怎么办 华为麦芒5太卡怎么办 小米note3拍照反应慢怎么办 华为刷机后还要账号密码怎么办 刷机后忘记华为账号和密码怎么办 荣耀7x耗电快怎么办 小米2s死机后怎么办? 电信合约卡不想用了怎么办 vivo合约机掉了怎么办 华为合约机丢了怎么办 两年合约机掉了怎么办 电信合约机丢了怎么办 s8合约机坏了怎么办 合约机的卡掉了怎么办 移动合约机屏幕碎了怎么办 5s用不了电信卡怎么办 vivo手机4g信号差怎么办 电信dns辅服务器未响应怎么办 笔记本wifi下载速度慢怎么办 苹果wifi下载速度慢怎么办 小米手机wifi下载速度慢怎么办 苹果8plus上网慢怎么办 小米5c死机了怎么办 苹果x自拍反方向怎么办 硅胶手机壳出油怎么办 指环扣松了怎么办图解 塑料放久了发粘怎么办 橡胶时间久了粘怎么办