redis源码浅析--十八.事务的实现

环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation
如有错误欢迎指正
参考书籍:《redis的设计与实现》
redis官网:https://redis.io/topics/transactions

文章推荐:
redis源码阅读-一--sds简单动态字符串
redis源码阅读--二-链表
redis源码阅读--三-redis散列表的实现
redis源码浅析--四-redis跳跃表的实现
redis源码浅析--五-整数集合的实现
redis源码浅析--六-压缩列表
redis源码浅析--七-redisObject对象(下)(内存回收、共享)
redis源码浅析--八-数据库的实现
redis源码浅析--九-RDB持久化
redis源码浅析--十-AOF(append only file)持久化
redis源码浅析--十一.事件(上)文件事件
redis源码浅析--十一.事件(下)时间事件
redis源码浅析--十二.单机数据库的实现-客户端
redis源码浅析--十三.单机数据库的实现-服务端 - 时间事件
redis源码浅析--十三.单机数据库的实现-服务端 - redis服务器的初始化
redis源码浅析--十四.多机数据库的实现(一)--新老版本复制功能的区别与实现原理
redis源码浅析--十四.多机数据库的实现(二)--复制的实现SLAVEOF、PSYNY
redis源码浅析--十五.哨兵sentinel的设计与实现
redis源码浅析--十六.cluster集群的设计与实现
redis源码浅析--十七.发布与订阅的实现
redis源码浅析--十八.事务的实现
redis源码浅析--十九.排序的实现
redis源码浅析--二十.BIT MAP的实现
redis源码浅析--二十一.慢查询日志的实现
redis源码浅析--二十二.监视器的实现

目录

一 事务的实现

1.1事务开始

1.2命令入队

1.3事务队列

1.4执行事务

二 watch 命令的实现

2.1使用watch监听数据库键

2.2监听机制的触发

三 事务的ACID属性

3.1原子性(atomicity)

Why Redis does not support roll backs?

3.2一致性(consistency)

3.3隔离型(isolation)

3.4持久性(durability)


Redis通过MULTI、EXEC、WATCH等命令实现事务。
Redis事务将多个命令打包,然后一次性、按照顺序执行命令请求;
在事务执行期间,服务器不会中断事务而去执行其他客户端请求;

  •  MULTI 标记一个事务块的开始
  • EXEC 执行所有事务块内的命令
  • DISCARD 取消事务,放弃执行事务块内的所有命令
  • WATCH key [key ...] 用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
  • UNWATCH 取消 WATCH 命令对所有 key 的监视

一 事务的实现

一个事务的执行会经历以下三个阶段:

  1. 事务开始
  2. 命令入队
  3. 事务执行

1.1事务开始

客户端状态的flag记录当前客户端是否在集群状态;

mult命令的实现只是想flag标记为集群状态;代码入口为multi.c/multiCommand


/**
 * multi命令的实现
 */ 
void multiCommand(client *c) {
    //MULTI 不可以嵌套调用
    if (c->flags & CLIENT_MULTI) {
        addReplyError(c,"MULTI calls can not be nested"); // nested 嵌套的
        return;
    }
    //设置client当前是事务状态
    c->flags |= CLIENT_MULTI;
    addReply(c,shared.ok);
}

 

1.2命令入队

当客户端处于事务状态时,redis会根据命令判断是否要立即执行,还是要放到队列当中;

当客户端发送是命令是EXEC、DISCARD、WATCH、MUITL命令时,会立即执行,否则会加入到事务队列中;
代码入口位于server.c/processCommand()
 

int processCommand(client *c) {

//................
      /**
     * 如果客户端当前在事务状态 
     * 并且执行的命令不是 EXEX、DISCARD、MUITL、WATCH,那么将命令放到事务队列中
     */ 
    /* 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;
}

 

1.3事务队列

redis每个客户端都有自己的事务状态,记录在了multiState属性中;
server.h/client
 

typedef struct client {
//.......
    multiState mstate;      /* MULTI/EXEC state */ //事务执行的状态

//......
}

server.h/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 minreplicas;        /* MINREPLICAS for synchronous replication */
    time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;

multiState结构体中multiCmd是个multiCmd数组, 保存着要执行的命令队列;
命令入队的入口位于:multi.c/queueMultiCommand()
 


/**
 * 将一个命令加入事务的执行队列中
 */ 
/* 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));

    //mc指向新命令要存入的地址,并将要写入的命令存入mc
    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]);

    //队列中的命令数量+1
    c->mstate.count++;
    c->mstate.cmd_flags |= c->cmd->flags;
}

 

1.4执行事务

服务端收到EXEC命令后,首先会判断事务完整性是否已经被破坏:

  • watch的key触发改变
  • 命令排队期间有错误

如果事务是安全的会 遍历所有事务队列中的命令,最后将执行结果返回给客户端;

exec的执行入口位于multi.c/execCommand()


/**
 * exec命令的实现
 */ 
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和SLAVE
    int was_master = server.masterhost == NULL;

    //客户端当前不在事务状态下
    if (!(c->flags & CLIENT_MULTI)) {
        addReplyError(c,"EXEC without MULTI");
        return;
    }

    /**
     * 检查是否需要终止exec的事务的执行,
     * 出现以下情况会终止执行:
     * 1) watch的key触发改变
     * 2) 命令排队期间有错误
     * 
     * 在第一种情况下,失败的EXEC返回一个multi_bulk_nil对象,
     * (从技术上讲,这不是一个错误,而是一种特殊的场景)
     * 第二种情况下,返回EXECABORT错误。
     */ 
    /* 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.nullmultibulk);
        //取消事务
        discardTransaction(c);
        goto handle_monitor;
    }

    /**
     * 如果事务中包含了写命令,但是当前服务器是一个slave,只有read权限,那么要发送错误
     * 
     * 什么情况下会发生这个场景:
     * 当事务启动的时候 服务器是master或者是一个具有写权限的replica;
     * 然后服务器的配置发生了改变,例如变成了replica,不能再执行事务了
     */ 
    /* If there are write commands inside the transaction, and this is a read
     * only slave, we want to send an error. This happens when the transaction
     * was initiated when the instance was a master or a writable replica and
     * then the configuration changed (for example instance was turned into
     * a replica). */
    if (!server.loading && server.masterhost && server.repl_slave_ro &&
        !(c->flags & CLIENT_MASTER) && c->mstate.cmd_flags & CMD_WRITE)
    {
        addReplyError(c,
            "Transaction contains write commands but instance "
            "is now a read-only slave. EXEC aborted.");
        discardTransaction(c);
        goto handle_monitor;
    }

    /**
     * 执行队列中的所有命令
     */ 
    /* Exec all the queued commands */

    //尽快取消 watch的key,否则会浪费CPU时钟
    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++) {
        c->argc = c->mstate.commands[j].argc;
        c->argv = c->mstate.commands[j].argv;
        c->cmd = c->mstate.commands[j].cmd;

        /**
         * 一旦我们遇到一个命令,它不是只读的,也不是管理性的,就传播一个多请求。
         * 通过这种方式,我们将MULTI/…/EXEC块作为一个整体交付,AOF和复制链接将具有相同的一致性和原子性保证
         */ 
        /* 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 && !(c->cmd->flags & (CMD_READONLY|CMD_ADMIN))) {
            execCommandPropagateMulti(c);
            must_propagate = 1;
        }

        /**
         * 执行命令
         * server.loading 正在从磁盘加载数据
         */ 
        call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);

        /**
         * 命令可能修改过了,重新存到mstate
         */
        /* 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));
        }
    }

//TODO 监视器的实现
handle_monitor:
    /**
     * 向等待监视器数据的客户端发送EXEC。
     * 我们在这里这样做是因为事务执行的自然顺序实际上是:
     * MUTLI,EXEC 事务中队列中的命令。。。
     * 相反,EXEC在command表中被标记为CMD_SKIP_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);
}

 

二 watch 命令的实现

watch命令是一个乐观锁,他可以在EXEC命令执行之前,监视任意数量的数据库键;
在exec命令执行的时检查是否有个一个键被修改过, 如果有,服务器将拒绝执行事务;

 

2.1使用watch监听数据库键

redis数据库有一个watched_keys字典,键是被监视的key,值则是clien链表,记录监视这个key的客户端信息;
server.h/redisDb:

/**
 * redis数据库描述
 */ 
/* Redis database representation. There are multiple databases identified
 * by integers from 0 (the default database) up to the max configured
 * database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
    //.....
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    //.....
} redisDb;

同样client中也有一个watched_keys列表,记录了当前clien 监视的所有key;
server.h/watched_keys

/**
 * 客户端
 * 对于多路复用,我们需要记录每个客户端的状态
 */ 
/* With multiplexing we need to take per-client state.
 * Clients are taken in a linked list. */
typedef struct client {
    //......
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */ //监视的key 列表 (cas乐观锁)
    //......    

} client;

所以当redis 监视一个key时,需要执行下面两个操作:

  1. 将监视key 添加到client->watched_keys 列表中;
  2. 将 客户端 添加 redisDb->watched_keysc的字典中(key指向的列表);

实现代码位于multi.c/watchForKey()


/**
 * 监视指定的数据库键
 */ 
/* Watch for the specified key */
void watchForKey(client *c, robj *key) {
    list *clients = NULL;
    listIter li;
    listNode *ln;
    watchedKey *wk;

    /**
     * 检查是否已经监视了指定key
     */ 
    /* 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 */
    }

    //检查DB中是否已经记录了监视这个key,如果没有这创建
    /* 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);

    //将key添加到 客户端端watched_keys中
    /* 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);
}

 

2.2监听机制的触发

所有对数据库进行的修改的操作,都会调用db.c/signalModifiedKey;(Hooks for key space changes.)

signalModifiedKey函数会调用multi.c/touchWatchedKey()函数;检查key是否被监视;

如果被监视,则会修改监视这个key的客户端的 CLIENT_DIRTY_CAS位,标记事务已经被破坏;

上面讲到过 EXEC执行的时候会检查CLIENT_DIRTY_CAS位,如果事务被破坏,EXEC将会不执行;

上源码multi.c/touchWatchedKey():


/**
 * 触发key,
 * 如果一个key被某些client监视, “touch key” ,后面执行EXEC将失败 
 */ 
/* "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;

    //db中没有key 被watch 直接返回
    if (dictSize(db->watched_keys) == 0) return;

    //监视参数key的clients的列表是空的,直接返回
    clients = dictFetchValue(db->watched_keys, key);
    if (!clients) return;

    /**
     * 将监视这个key的client的CLIENT_DIRTY_CAS位标记为dirty
     */ 
    /* 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;
    }
}

 

三 事务的ACID属性

 

3.1原子性(atomicity)

原子性是指 数据库将事务中所有操作当作一个整体来执行,要么执行所有操作,要么都不执行;

和传统的关系型数据库最大的区别在于 redis不支持事务的回滚;

在事务队列执行阶段,如果某个命令执行出错,整个事务还是回执行下去,直到队里中所有命令执行完毕;

redis官网对为什么不支持回滚做了下面的解释:

Why Redis does not support roll backs?

If you have a relational databases background, the fact that Redis commands can fail during a transaction, but still Redis will execute the rest of the transaction instead of rolling back, may look odd to you.

However there are good opinions for this behavior:

  • Redis commands can fail only if called with a wrong syntax (and the problem is not detectable during the command queueing), or against keys holding the wrong data type: this means that in practical terms a failing command is the result of a programming errors, and a kind of error that is very likely to be detected during development, and not in production.
  • Redis is internally simplified and faster because it does not need the ability to roll back.

An argument against Redis point of view is that bugs happen, however it should be noted that in general the roll back does not save you from programming errors. For instance if a query increments a key by 2 instead of 1, or increments the wrong key, there is no way for a rollback mechanism to help. Given that no one can save the programmer from his or her errors, and that the kind of errors required for a Redis command to fail are unlikely to enter in production, we selected the simpler and faster approach of not supporting roll backs on errors.

=======

  • 只有在使用了错误的语法的时候才会导致命令的执行失败(在命令入队的时候无法检测的语法错误),或是使用了key不支持的错误数据类型;这就意味着 执行错误的命令是编程错误导致的;
    这种错误在开发阶段大概率就发现了这种错误,而不是在生成环境;
  • 保持redis 内部实现的简单高效;

 

3.2一致性(consistency)

事务一致性是指 ,如果数据库执行事务之前是一致的,那么在事务执行之后,无论事务是否成功,数据库也应该是一致的;

redis通过错误检查和简单的设计来保证一致性

  1. 入队错误
    一个事务在命令入队的过程中会检查,是否存在语法错误,如果有错误将会导致事务不执行;
  2. 执行错误
    在命令执行的时候,仍然有可能发生错误;但是并不会中断事务的执行
    并且已经执行过的命令也不会收到影响

3.3隔离型(isolation)

隔离性是指  即使数据库中有多个事务并发执行的时候,各个事务之间也不会相互影响
并且在串行执行和并发执行 事务执行的结果完全相同;

因为redis使用单线程方式执行,服务保证事务执行期间不会对事务中断,因为redis事务总是串行方式进行;具有隔离性

 

3.4持久性(durability)

事务的持久性是指 当一个事务执行完毕后,执行这个事务的结果被永久性存储介质;

redis的事务也是简单的包裹了一层redis命令,并没有为事务额外提供持久化功能

所以redis事务的持久性取决于所使用的持久化模式;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值