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设计与实现

posted @ 2020-10-12 14:40  hulunbao  阅读(148)  评论(0编辑  收藏  举报