zoukankan      html  css  js  c++  java
  • 关于Redis的ACID

    事务是一个数据库必备的元素,对于redis也不例外,对于一个传统的关系型数据库来说,数据库事务满足ACID四个特性:

    • A代表原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
    • C代表一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束
    • I代表隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行
    • D代表持久性:已被提交的事务对数据库的修改应该永久保存在数据库中

    然而,对于redis来说,只满足其中的:

    一致性和隔离性两个特性,其他特性是不支持的。

    关于redis对ACID四个特性暂时先说这么多,在本文后面会详细说明。在详述之前,我们先来了解redis事务具体的使用和实现,这样我们接下来讨论ACID时才能更好的理解。

    redis事务的使用

    redis事务主要包括MULTI、EXEC、DISCARD和WATCH命令

    MULTI命令

    MULTI命令用来开启一个事务,当MULTI执行之后,客户端可以继续向服务器发送多条命令,这些命令会缓存在队列里面,只有当执行EXEC命令时,这些命令才会执行。

    而DISCARD命令可以清空事务队列,放弃执行事务。

    一个使用MULTI和EXEC执行事务的例子:

    redis> MULTI
    OK
    
    redis> SET book-name "Mastering C++ in 21 days"
    QUEUED
    
    redis> GET book-name
    QUEUED
    
    redis> SADD tag "C++" "Programming" "Mastering Series"
    QUEUED
    
    redis> SMEMBERS tag
    QUEUED
    
    redis> EXEC
    1) OK
    2) "Mastering C++ in 21 days"
    3) (integer) 3
    4) 1) "Mastering Series"
       2) "C++"
       3) "Programming"

    一个事务从开始到执行会经历以下三个阶段:

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

    redis事务产生错误

    使用redis事务可能会产生错误,主要分为两大类:

    • 事务在执行EXEC之前,入队的命令可能出错。
    • 命令可能在 EXEC 调用之后失败

    redis在2.6.5之后,如果发现事务在执行EXEC之前出现错误,那么会放弃这个事务。

    在EXEC命令之后产生的错误,会被忽略,其他正确的命令会被继续执行。

    redis事务不支持回滚

    如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。

    以下是这种做法的优点:

    • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
    • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。

    鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。

    关于WATCH命令

    WATCH命令可以添加监控的键,如果这些监控的键没有被其他客户端修改,那么事务可以顺利执行,如果被修改了,那么事务就不能执行。

    redis> WATCH name
    OK
    
    redis> MULTI
    OK
    
    redis> SET name peter
    QUEUED
    
    redis> EXEC
    (nil)
    

    以下执行序列展示了上面的例子是如何失败的:

    时间客户端 A客户端 B
    T1 WATCH name  
    T2 MULTI  
    T3 SET name peter  
    T4   SET name john
    T5 EXEC  

    在时间 T4 ,客户端 B 修改了 name 键的值, 当客户端 A 在 T5 执行 EXEC 时,Redis 会发现 name 这个被监视的键已经被修改, 因此客户端 A 的事务不会被执行,而是直接返回失败

    当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查:

    • 如果客户端的 REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。
    • 如果 REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。

    这里是伪代码

    def check_safety_before_execute_trasaction():
    
        if client.state & REDIS_DIRTY_CAS:
            # 安全性已破坏,清除事务状态
            clear_transaction_state(client)
            # 清空事务队列
            clear_transaction_queue(client)
            # 返回空回复给客户端
            send_empty_reply(client)
        else:
            # 安全性完好,执行事务
            execute_transaction()
    

    redis事务的实现

    在了解了redis事务的使用之后,我们再来看看redis事务的实现,主要是对上面说的MULTI、EXEC、DISCARD和WATCH命令的源码的分析。

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

    从上面的源码可以看出,MULTI只能执行一次,而且就做一件事,把客户端的标志打上REDIS_MULTI。

    EXEC命令实现

    void execCommand(redisClient *c) {
        int j;
        robj **orig_argv;
        int orig_argc;
        struct redisCommand *orig_cmd;
    
        if (!(c->flags & REDIS_MULTI)) {
            addReplyError(c,"EXEC without MULTI");
            return;
        }
    
        /* Check if we need to abort the EXEC if some WATCHed key was touched.
        * A failed EXEC will return a multi bulk nil object. */
        if (c->flags & REDIS_DIRTY_CAS) {
            freeClientMultiState(c);
            initClientMultiState(c);
            c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
            unwatchAllKeys(c);
            addReply(c,shared.nullmultibulk);
            goto handle_monitor;
        }
    
        /* Replicate a MULTI request now that we are sure the block is executed.
        * 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. */
        execCommandReplicateMulti(c);
    
        /* 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++) {
            c->argc = c->mstate.commands[j].argc;
            c->argv = c->mstate.commands[j].argv;
            c->cmd = c->mstate.commands[j].cmd;
            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;
        freeClientMultiState(c);
        initClientMultiState(c);
        c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
        /* Make sure the EXEC command is always replicated / AOF, since we
        * always send the MULTI command (we can't know beforehand if the
        * next operations will contain at least a modification to the DB). */
        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);
    }

    其主要步骤是:

    • 检查是否已经是处在MULTI状态下,如果不是,直接返回
    • 检查WATCH的key是否已经被其他客户端修改,如果是,放弃事务
    • 执行事务命令队列里面的所有命令

    执行命令队列里面的所有命令的代码如下:

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

    遍历queue队列的所有命令,一条条的执行

    DISCARD命令实现

    DICARD命令主要由下面两个函数完成:

    void discardCommand(redisClient *c) {
        if (!(c->flags & REDIS_MULTI)) {
            addReplyError(c,"DISCARD without MULTI");
            return;
        }
        discardTransaction(c);
        addReply(c,shared.ok);
    }
    
    void discardTransaction(redisClient *c) {
        freeClientMultiState(c);
        initClientMultiState(c);
        c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);;
        unwatchAllKeys(c);
    }
    
    void freeClientMultiState(redisClient *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);
    }

    函数调用的关系如下

      discardCommand
           |
           |
           |
    discardTransaction
           |
           |
           |
    freeClientMultiState
    

    freeClientMultiState就是完成DISCARD命令主要功能的函数,把其命令队列里面的所有命令所在的存储空间都释放。

    WATCH命令

    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 lits of keys watched by this client */
        wk = zmalloc(sizeof(*wk));
        wk->key = key;
        wk->db = c->db;
        incrRefCount(key);
        listAddNodeTail(c->watched_keys,wk);
    }

    这个函数是在WATCH命令执行后,主要完成两件事情:

    • 把key添加到当前客户端c->watched_keys的链表中,
    • 并且把当前客户端添加到c->db->watched_keys的字典里面去。

    当db中有能修改键状态的命令(比如INCR等)执行时,会自动把watch这个key的客户端的事务设置为放弃状态:

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

    如上源码所示,当某个key被修改后,会遭到其字典中所在的hash链表,然后把链表中所有的客户端都设置成REDIS_DIRTY_CAS状态,即事务被DISCARD的状态。

    redis事务的ACID性质讨论

    A原子性

    单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的

    如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功

    另一方面,如果 Redis 服务器进程在执行事务的过程中被停止 —— 比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败

    事务失败时,Redis 也不会进行任何的重试或者回滚动作,不满足要么全部全部执行,要么都不执行的条件

    C一致性

    一致性分下面几种情况来讨论:

    首先,如果一个事务的指令全部被执行,那么数据库的状态是满足数据库完整性约束的

    其次,如果一个事务中有的指令有错误,那么数据库的状态是满足数据完整性约束的

    最后,如果事务运行到某条指令时,进程被kill掉了,那么要分下面几种情况讨论:

    • 如果当前redis采用的是内存模式,那么重启之后redis数据库是空的,那么满足一致性条件
    • 如果当前采用RDB模式存储的,在执行事务时,Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执行之后,保存 RDB 的工作才有可能开始。所以当 RDB 模式下的 Redis 服务器进程在事 务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里。 恢复数据库需要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数 据库快照(snapshot),所以它的数据可能不是最新的,但只要 RDB 文件本身没有因为 其他问题而出错,那么还原后的数据库就是一致的

    • 如果当前采用的是AOF存储的,那么可能事务的内容还未写入到AOF文件,那么此时肯定是满足一致性的,如果事务的内容有部分写入到AOF文件中,那么需要用工具把AOF中事务执行部分成功的指令移除,这时,移除之后的AOF文件也是满足一致性的

    所以,redis事务满足一致性约束

    I隔离性

    Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。

    D持久性

    因为事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定

    • 在单纯的内存模式下,事务肯定是不持久的
    • 在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的
    • 在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用 fsync 或 fdatasync 将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。
    • 其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。

    来源:https://github.com/Charles0429/redis_doc/blob/master/function/redis_transaction.md

    http://redisbook.readthedocs.org/en/latest/feature/transaction.html

  • 相关阅读:
    Java中运算符“|”和“||”以及“&”和“&&”区别
    idataway_前端
    web前端名人的博客微博Githu
    css动效库animate.css和swiper.js
    elementUI和iview兼容么
    calc
    多年未写过java了
    ajax
    commonjs
    优秀的移动端设计
  • 原文地址:https://www.cnblogs.com/chenpingzhao/p/5001894.html
Copyright © 2011-2022 走看看