zoukankan      html  css  js  c++  java
  • Redis 事务

    事务

    事务指的程序中一系列严密的逻辑操作,其中包含的操作必须要完成,否则在每个操作中的更改都会被撤销。

    举个简单的例子:一群鸭子过河,要么都过去,要么都不过去。

    事务的特性

    1. 原子性(Atomicity):操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。
    2. 一致性(Consistency):事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。(可理解为:即A账户只要减去了100,B账户则必定加上了100)
    3. 隔离性(Isolation):隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
    4. 持久性(Durability):当事务正确完成后,它对于数据的改变是永久性的。

    为什么使用事务

    在传统的关系型数据库中,常常使用事务的ACID 性质来保证数据的一致性、完整性。

    Redis事务

     Redis事务:把多个redis命令放到队列,然后一次性的顺序执行。并且在执行过程中不会被中断,执行完所有队列命令后才执行其他客户端其他命令。

    下面举个简单的例子,MULTI 开启一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令

     

     一个事务从开始到执行经历三个阶段

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

    开始事务

    MULTI 命令的执行标志着事务的开始,这个命令做的就是, 将客户端的 REDIS_MULTI 选项打开, 让客户端从非事务状态切换到事务状态。

     命令入队

    当客户端处于非事务状态下,所有发送给服务端的命令会立即被执行。

    但客户端切换到事务状态后,服务端接受到客户端的命令不会立即执行,而是把这些命令放到事务队列里,然后返回 QUEUED , 表示命令已入队。

     可以由一下流程图表示:

    事务队列是一个数组, 每个数组项是都包含三个属性:

    1. 要执行的命令(cmd)
    2. 命令的参数(argv)
    3. 参数的个数(argc)

     以上图命令为例子,那么程序将为客户端创建以下事务队列:

    数组索引 cmd argv argc
    0 SET ["name","xiaoming"] 2
    1 SET ["age","25"] 2
    2 INCR ["age"] 1

     

     

    执行事务

    客户端进入到事务状态后,客户端发送的命令不会直接被执行,而是会放到事务队列里。

    但并不是所有命令都会放到事务队列,如 EXEC 、 DISCARD 、 MULTI 和 WATCH 这四个命令,无视事务状态,直接被服务器执行。

     

     如果客户端正处于事务状态, 那么当 EXEC 命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令: 最先入队的命令最先执行, 而最后入队的命令最后执行。

    执行事务中的命令所得的结果会以 FIFO 的顺序保存到一个回复队列中。

    如上图,程序将为队列中的命令创建如下回复队列:

    数组索引 回复类型 回复内容
    0 status code reply OK
    1 status code reply OK
    2 integer reply 26

    当事务队列里的所有命令被执行完之后, EXEC 命令会将回复队列作为自己的执行结果返回给客户端, 客户端从事务状态返回到非事务状态, 至此, 事务执行完毕。

    事务过程伪代码:

    def execute_transaction():
    
        # 创建空白的回复队列
        reply_queue = []
    
        # 取出事务队列里的所有命令、参数和参数数量
        for cmd, argv, argc in client.transaction_queue:
    
            # 执行命令,并取得命令的返回值
            reply = execute_redis_command(cmd, argv, argc)
    
            # 将返回值追加到回复队列末尾
            reply_queue.append(reply)
    
        # 清除客户端的事务状态
        clear_transaction_state(client)
    
        # 清空事务队列
        clear_transaction_queue(client)
    
        # 将事务的执行结果返回给客户端
        send_reply_to_client(client, reply_queue)

    DISCARD、MULTI、WATCH命令

    • DISCRAD 命令用于取消一个事务, 它清空客户端的整个事务队列, 然后将客户端从事务状态调整回非事务状态, 最后返回字符串 OK 给客户端, 说明事务已被取消。
    • MULTI命令开启一个事务,Redis 的事务是不可嵌套的, 当客户端已经处于事务状态, 而客户端又再向服务器发送 MULTI 时, 服务器只是简单地向客户端发送一个错误, 然后继续等待其他命令的入队。 MULTI 命令的发送不会造成整个事务失败, 也不会修改事务队列中已有的数据。
    • WATCH命令只能在客户端进入事务状态之前执行, 在事务状态下发送 WATCH 命令会引发一个错误, 但它不会造成整个事务失败, 也不会修改事务队列中已有的数据(和前面处理 MULTI 的情况一样)

    带 WATCH 的事务

    WATCH 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。

    如下图例子:

    客户端A

     此时客户端B修改了name

     客户端A执行事务失败

     watch命令实现

    在每个代表数据库的 redis.h/redisDb 结构类型中, 都保存了一个 watched_keys 字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端。

    如下图:

     WATCH 命令的作用, 就是将当前客户端和要监视的键在 watched_keys 中进行关联。

    举个例子,如果客户端client5执行 WATCH key1 key2 时,上图将变成下面这样。

     通过 watched_keys 字典, 如果程序想检查某个键是否被监视, 那么它只要检查字典中是否存在这个键即可; 如果程序要获取监视某个键的所有客户端, 那么只要取出键的值(一个链表), 然后对链表进行遍历即可。

    WATCH 触发

    任何对Redis键值的修改操作成功后,multi.c/touchWatchedKey 函数都会被调用,它检查数据库的 watched_keys 字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开:

    当客户端发送 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()

    最后,当一个客户端结束它的事务时,无论事务是成功执行,还是失败, watched_keys 字典中和这个客户端相关的资料都会被清除。

    Redis事务的ACID性质

    Redis事务具有原子性、一致性、隔离性,并且当Redis运行在某种特定的持久化模式下,也具有持久性。

    原子性

    Redis事务,事务队列中的命令要么全部执行,要么一个都不执行。所以Redis事务具有原子性。

    Redis事务与传统的关系型数据库事务最大的区别在于:Redis事务不支持回滚机制(rollback),即使事务队列中的命令执行期间出现了错误,整个事务会继续下去,直到事务队列里面的命令都执行完。

    如下例子:

     一致性

    Redis通过错误检测和简单的设计保证事务一致性。

    • 入队错误

    如果一个事务在入队过程中,出现了命令不存在,或者命令格式不正确,Redis拒绝执行这个事务。

    如下例子

     因为拒掉入队错误的事务,所以一致性不会被入队错误的事务影响。

    • 执行错误

    事务执行过程中,错误的命令会被识别出来,并进行相应的错误处理,所以一致性不会受到影响。

    • 服务器停机

    如果服务器运行在没有持久化的内存模式下,那么重启后数据库是空白的,因此数据是一致的。

    如果服务器运行在RDB或者AOF模式下,中途停机可以用两种模式恢复,如果找不到恢复文件,数据库是空白的,数据是一致的。

    隔离性

    Redis采用的单线程的方式实行事务,并且在事务执行过程中不会对事务中断,因此Redis事务以串行的方式运行的,所以Redis事务具有隔离性。

    持久性

    Redis事务,用队列保存了一些列命令,没有为事务提供额外的持久化功能,所有Redis的持久化由Redis的持久化模式决定。

    • 没有持久化

    Redis没有开启持久化模式,事务不具有持久性,一旦服务器停机,包括事务在内的数据都将丢失

    • RDB

    当服务器运行在RDB模式下,服务器只在特定的条件满足时,才会执行BGSAVE命令保存数据库数据。然而异步执行的BGSAVE命令不能保证事务数据第一时间保存到磁盘。

    因此RDB模式下的事务不具备持久性。

    • AOF

    当服务器运行在AOF模式下,并且appendfsync 选项的值为 always 时,程序总会在执行命令后调用调用 sync 函数,将数据同步到磁盘中,因此在此场景中AOF是具有持久性的。

    其他情况下,不能保证每执行一个命令就能将数据同步到磁盘,所以事务不具备持久性。

    参考文献

    https://cloud.tencent.com/developer/article/1133074

    http://redisdoc.com/topic/transaction.html

    reids设计与实现

  • 相关阅读:
    利用heroku+mongoLab 部署Node 运用
    css常见解决方案
    JavaScript数组基本用法
    ES5中数组的新方法
    JavaScript闭包详解
    力扣第991题 坏了的计算器
    力扣第1189题 “气球” 的最大数量
    力扣第142题 环形链表 II
    力扣第260题 只出现一次的数字 III
    力扣第141题 环形链表
  • 原文地址:https://www.cnblogs.com/hulunbao/p/13800152.html
Copyright © 2011-2022 走看看