• redis学习笔记(八): multi


    redis实现了对"事务"的支持,核心函数都在这里
    摘抄对于事务的定义:是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行
    它的4个特性:原子性、一致性、隔离性、持久性
    redis在事务的执行中并没有提供回滚操作,它会按顺序执行完队列中的所有命令而不管中间是否有命令出错(当然,执行出错的命令会打印出错信息),所以一致性没办法保障。

    相关的command:

    struct redisCommand redisCommandTable[] = {
        ...
        {"multi",    multiCommand,   1,"rsF",0,NULL,0,0,0,0,0},    //标识事务的开始
        {"exec",     execCommand,    1,"sM",    0,NULL,0,0,0,0,0},    //事务的提交(commit)
        {"discard",  discardCommand, 1,"rsF",0,NULL,0,0,0,0,0},    //取消事务(不是回滚)
        ...
        {"watch",    watchCommand,  -2,"rsF",0,NULL,1,-1,1,0,0},
        {"unwatch",  unwatchCommand, 1,"rsF",0,NULL,0,0,0,0,0},
        ...
    }

    初始化一个multi state,就是简单地将commands指针设置为空,count设置为0。代码如下:

    /* Client state initialization for MULTI/EXEC */
    void initClientMultiState(redisClient *c) {
        c->mstate.commands = NULL;
        c->mstate.count = 0;
    }

    queueMultiCommand用来将命令加入待执行队列,MULTI到EXEC之间的命令都是通过它来入队的,核心代码如下:

    /* 把待执行的command加入到队列。
     * 每次来一个新的command就要realloc一次空间,而且只增加一个command的大小。
     * 为什么不用预分配再适当扩大的办法?类似于sdsMakeRoomFor的做法?
     */
    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++;
    }

    discardTransaction用来取消某一个事务,discard命令、exec返回之前都会调用它。它除了释放mstate.commands数组之外,最后还会unwatch all keys

    void discardTransaction(redisClient *c) {
        freeClientMultiState(c);
        initClientMultiState(c);
        c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);
        unwatchAllKeys(c);
    }

    flagTransaction用来把一个进入multi状态的client打上标记:REDIS_DIRTY_EXEC

    /* Flag the transacation as DIRTY_EXEC so that EXEC will fail.
     * Should be called every time there is an error while queueing a command. */
    /* 只在processCommand中发现是不合法的命令时会被调用 
     * 问题是,在processCommand当中,调用这个函数之后都返回OK了,不会进行后面的处理(比如执行或者queue),打这个标记有什么作用?
     * EXEC在执行之前会检查是否有这个标记,只要有这个标记,就返回错误并取消事务
     * 所以,事务中的一系列命令,只要有一个命令的格式错误,其它的全部不执行
     */
    void flagTransaction(redisClient *c) {
        if (c->flags & REDIS_MULTI)
            c->flags |= REDIS_DIRTY_EXEC;
    }

    multiCommand是multi命令的处理入口,它只是检查状态之后,简单地将client标记为REDIS_MULTI状态

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

    discard命令的处理函数

    void discardCommand(redisClient *c) {
        if (!(c->flags & REDIS_MULTI)) {
            addReplyError(c,"DISCARD without MULTI");
            return;
        }
        discardTransaction(c);
        addReply(c,shared.ok);
    }

    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? */
    
        /* 必须是MULTI状态 */
        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. */
        /* 执行之前,检查一下异常状态。其中REDIS_DIRTY_CAS会在touchWatchedKey中被打上,这样就实现了原子操作 */
        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 */
        /* 为什么不做unwatch就会浪费cpu?
         * 因为下面要执行的一系列命令可能会修改某些key,如果不unwatch all,就可能会做一些不必要的touchWatchedKey操作?
         */
        unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
        /* 记录原始(当前)的cmd相关指针 */
        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. */
            /* 如果有写命令,只进行一次propagate,保证AOF和replication的一致性和原子性 */
            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. */
        /* 如果执行过propagate,dirty计数加1 */
        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. */
        /* 如果有client在monitor上等待输出(监控?),将这次的命令信息(不是MULTI...EXEC之间执行的命令,MULTI...EXEC之间的命令在上面执行call的时候会发到monitor)发送给相应的client */
        if (listLength(server.monitors) && !server.loading)
            replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
    }

    上面的部分是multi命令执行需要的所有相关函数。但是仅仅只有上面的部分的话,也只是实现了一种"批量处理"的方式,还不能算是事务。下面提到的watch就是用来保证原子性。

    代码中,对WATCH的注释是: CAS alike for MULTI/EXEC。

    CAS应该是Compare and Swap,是一种实现乐观锁的机制,它的原理:认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可

    具体到用watch机制来保证操作的原子性(下面这个加1的操作可以用incr一条命令实现,这里只是为了演示):

    1. watch key
    2. val = get key
    3. val = val + 1
    4. MULTI
    5. set key value
    6. EXEC
    对于上面的这一系列操作,如果在EXEC命令之前,有其它client修改了key对应的value,那么这一次的EXEC是不会执行的,需要重新执行上面的所有步骤(EXEC结束时会unwatch all keys)。
    所以redis里事务的原子性必须要依靠watch来保证。

    watch的实现中使用了下面这个结构体,用来将key和db进行关联

    /* ===================== 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 */
    /* redisClient结构中用list来组织该client watch的所有keys
     * redisDB结构中用dict来组织watch某一个key的所有client列表
     */
    typedef struct watchedKey {
        robj *key;
        redisDb *db;
    } watchedKey;

    watch key的核心操作

    /* 
     * 1. 如果client的watched_keys列表上已经有了这个key,直接返回
     * 2. 如果没有,则加到相应的db中,再加到client的watched_keys列表上
     * 3. 增加这个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 */
        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);
    }

    unwatchAllKeys用来unwatch某个client所有watched keys

    /* Unwatch all the keys watched by this client. To clean the EXEC dirty
     * flag is up to the caller. */
    /* unwatch某个client watch过的所有keys,主要操作:
     * 1. 从db中watched_keys上相应key上的client列表中移除该client
     * 2. 从该client的watched_keys中移除所有元素
     */
    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 */
            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 */
            listDelNode(c->watched_keys,ln);
            decrRefCount(wk->key);
            zfree(wk);
        }
    }

    touchWatchedKey函数是保证原子性的一部分操作:

    /* touchWatchedKey只会被signalModifiedKey调用,所以应该是某个key对应的value被改的时候会走到这里? 
     * 它只是简单地打标记,在执行EXEC命令时如果有这个标记,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 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;
        }
    }

    在将db的内容写到磁盘上时,会调用touchWatchedKeysOnFlush

    /* 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). */
    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))) {
            redisClient *c = listNodeValue(ln);
            listRewind(c->watched_keys,&li2);
            while((ln = listNext(&li2))) {
                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 be
                 * removed. */
                if (dbid == -1 || wk->db->id == dbid) {
                    if (dictFind(wk->db->dict, wk->key->ptr) != NULL)
                        c->flags |= REDIS_DIRTY_CAS;
                }
            }
        }
    }

    最后,watch/unwatch命令的入口

    /* watch命令的入口函数 */
    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);
    }
    
    /* unwatch命令的入口函数 */
    void unwatchCommand(redisClient *c) {
        unwatchAllKeys(c);
        c->flags &= (~REDIS_DIRTY_CAS);
        addReply(c,shared.ok);
    }
  • 相关阅读:
    orcle id和执行计划(转)
    mysql 授权
    nginx+php 安装手册
    为 MySQL 增加 HTTP/REST 客户端:MySQL UDF 函数 mysql-udf-http 1.0 发布
    Nginx提示502和504错误的解决方案
    error while loading shared libraries: xxx.so.x"错误的原因和解决办法
    lnmp memcache出问题
    Nginx下实现pathinfo及ThinkPHP的URL Rewrite模式支持
    Nginx代理与负载均衡配置与优化
    curl+ post/get 提交
  • 原文地址:https://www.cnblogs.com/flypighhblog/p/7764118.html
Copyright © 2020-2023  润新知