• redis 事务,深入解读



    前言

    本文参考源码版本:redis-6.2

    事务,古老而神秘的词汇,说起它,你应该能想起它的四大特性:原子性、隔离性、持久性和一致性。

    我们先往简单了想,事务解决了什么问题?确保一揽子修改操作的 正确性一致性

    事务需要做什么?本质是做了两件事,控制并发故障恢复

    redis 也提供了事务功能,不过,基于 redis 的一些特性,会在传统事务特性上做了一些宽松处理,也就是说,redis 的事务并非传统意义上的 “严格型事务”。

    我们先来回忆下,事务的的四大特性及其概念:

    原子性:一批操作要么一起成功,要么一起失败。这是事务的故障恢复要做的事,可以通过重试让所有操作都成功,也可以通过日志进行回滚让所有操作都失败。

    隔离性:事务之间的操作互不影响,即 事务之间,相互不可见。这是事务的控制并发模块要做的事情,比如 MySQL 提供的多种隔离级别:读未提交、读提交、可重复读和串行化。

    持久性:落盘了的操作,实实在在的存在,不会因为服务重启什么的丢失。

    一致性:事务的执行不会破坏数据库的完整性约束,包括数据关系的完整性和业务逻辑的完整性。

    接下来,我们进入 redis 的相关事务设计,看看有哪些差异需要注意~


    一、动手试试?

    redis 中,事务是以 multi 指令开始,后续的指令会提交到服务端队列缓存,直到提交 exec 指令时,将会从缓存队列中批量取出指令执行,最后响应客户端:

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set key1 ok
    QUEUED
    127.0.0.1:6379> get key2
    QUEUED
    127.0.0.1:6379> set name June
    QUEUED
    127.0.0.1:6379> get name
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) "hello"
    3) OK
    4) "June"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    我们继续看看,当遇到一些异常情况,redis 事务将如何应对:

    案例 1:指令成功进入缓存队列(返回 QUEUED),但语法有问题:

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set multi_key 111 hhh
    QUEUED
    127.0.0.1:6379> set multi_value 520
    QUEUED
    127.0.0.1:6379> get multi_value
    QUEUED
    127.0.0.1:6379> exec
    1) (error) ERR syntax error
    2) OK
    3) "520"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们可以看到,虽然第一条指令在执行阶段抛出了异常,但后续指令仍然成功执行。

    案例 2:指令或者参数错误,导致入队失败,整个事务将被取消:

    127.0.0.1:6379> multi 
    OK
    127.0.0.1:6379> set multi_key haloareyouok
    QUEUED
    127.0.0.1:6379> set
    (error) ERR wrong number of arguments for 'set' command
    127.0.0.1:6379> set multi_key2 haloareyouok2
    QUEUED
    127.0.0.1:6379> exec
    (error) EXECABORT Transaction discarded because of previous errors.
    127.0.0.1:6379> get multi_key
    (nil)
    127.0.0.1:6379> get multi_key2
    (nil)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    事务被取消,也就意味着任何指令都没有成功执行。


    综上,可以得出结论,redis 提供的是 弱原子性 保障。

    二、原理

    redis 的事物主要依靠 MULTI、EXEC、DISCARD 等指令实现,同时,还提供了 WATCH、UNWATCH 来进一步扩展事务的能力:

    • MULTI、EXEC 用于开启和提交事务
    • DISCARD 用于取消事务
    • WATCH、UNWATCH 用于乐观锁

    1.事务实现

    redis 的事务分为三个步骤:事务开启指令提交事务提交,如下:

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set key hello
    QUEUED
    127.0.0.1:6379> get key
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) "hello"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    以上例子包含了一个完整的 redis 事务使用:

    • 事务提交:通过执行 multi 指令
    • 指令提交:案例中的 set、get 等指令
    • 事务提交:通过执行 exec 指令

    在服务端的 client 结构体中,有如下定义:

    typedef struct client {
    
        ...
        uint64_t flags;        
        multiState mstate;
    
        ...
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • flags 标志:当我们提交 multi 指令时,将会加上 CLIENT_MULTI 标志,即 事务开启
    • multiState:命令队列,即 记录所有事务中的指令

    服务端处理流程上大致是这样:

    • 当客户端开启事务时,将 flag 字段加上 CLIENT_MULTI 标志
    • 然后,持续接收客户端发过来的指令,并存储在 multiState 队列中
    • 当客户端提交事务时,从 multiState 队列中取出所以指令并按顺序执行

    从实现上来看,redis 的事务相关逻辑十分简单,当然,这也是为了性能,牺牲了部分原子性保障。

    可以开启事务,自然也会有配套的取消事务,通过指令 discard 来完成:

    127.0.0.1:6379> discard
    OK
    
    • 1
    • 2

    2.乐观锁

    当多个客户端对相同的 key 并发做修改时,可能会出现不可预期的结果;我们通常做法时,在应用层全局加锁处理,当然,这这种叫悲观锁

    redis 在事务中提供了更加高效的乐观锁机制,通过 WATCHUNWATCH 指令可以监听或取消监听待处理的 key。

    1)案例:

    客户端 1 执行:

    127.0.0.1:6379> watch key1 key2 key3
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set key1 value1
    QUEUED
    127.0.0.1:6379> set key2 value2
    QUEUED
    127.0.0.1:6379> set key3 value3
    QUEUED
    127.0.0.1:6379> get key1
    QUEUED
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    接着,客户端 2 执行:

    127.0.0.1:6379> set key2 other_modify_it
    OK
    
    • 1
    • 2

    最后,继续客户端 1 执行,提交事务:

    127.0.0.1:6379> exec
    (nil)
    127.0.0.1:6379> get key1
    "ok"
    
    • 1
    • 2
    • 3
    • 4

    发现了吧,事务没执行! 由于我们在客户端 1 监听了 key1、key2、key3 三个关键字之后,如果被监听的 key 被其他客户端修改了,那么客户端 1 的事务将被取消。

    我们再来看成功执行的例子,是这样:

    127.0.0.1:6379> watch key1 key2
    OK
    127.0.0.1:6379> multi 
    OK
    127.0.0.1:6379> set key1 value1
    QUEUED
    127.0.0.1:6379> set key2 value2
    QUEUED
    127.0.0.1:6379> get key1
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) OK
    3) "value1"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    当然,事务提交之后,WATCH 的生命周期也就结束了。另外,我们还可以通过 unwatch 指令,手动取消监听:

    127.0.0.1:6379> unwatch
    OK
    
    • 1
    • 2

    2)原理:

    在 client 结构体中,有如下定义:

    typedef struct client {
    
        ...
        
        uint64_t flags;    
        list *watched_keys;  // 链表结构,指向 watchedKey 类型的链表
     
        ...
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    watchedKey 结构体:

    typedef struct watchedKey {
        robj *key;  // key
        redisDb *db; // 所在 db
    } watchedKey;
    
    • 1
    • 2
    • 3
    • 4

    client 的 watched_keys 字段指向的是 watchedKey 类型的链表,会记录每个监听 key 所在 db。

    修改通知:redis 提供的修改指令,在执行之后,会通知 watched_keys 列表;对于匹配上的客户端,主要是在其 flags 字段加上 CLIENT_DIRTY_CAS 标志,表示监听的 value 已经“脏了”(被更新)。

    事务终止:事务提交时,会在执行所有事务指令之前检查 flags 字段是否有 CLIENT_DIRTY_CAS 标志,如果有,则直接取消事务,当然,也就不会执行任何指令。

    3.ACID 特性

    1)原子性:

    事务中的所有操作都会进入服务端暂存队列,进入该队列之前,会检查命令的合法性,但不会检查语法的合法性。

    语法的合法性要在执行的时候才能体现出来,如果语法有问题,redis 会忽略该指令,但会继续执行剩余指令。

    这里你可能会问了,这不满足原子性的要求啊。是的,这里是伪原子性

    redis 的作者认为,执行阶段的语法错误会在软件测试阶段提前暴露并修正;并且,redis 定位是快速响应的内存数据库,如果加入回滚能力,将会严重影响效率。

    2)隔离性:

    redis 服务端命令是串行执行,因此,天然具备隔离性

    3)持久性:

    这个依赖于 redis 服务端选择的持久化类型。如果你选择的是 RDB 持久化,丢失数据可能会多一些。如果你选择 AOF 或者 AOF/RDB 混合模式,丢失数据风险就小的多。

    如果选择有 AOF 的模式,并且参数 appendfsync = everysec 时,redis 具备持久性;不过,需要注意的是,如果配置 no-appendfsync-on-rewrite = yes,在 BGSAVE 或者 AOF rewrite 期间也是不具备持久化的。

    其他模式下,由于存在数据丢失风险,因此,不具备持久化能力。

    4)一致性:

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

    “一致” 指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。redis 通过谨慎的错误检测和简单的设计来保证事务的一致性

    • 入队错误:如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况,那么 redis 将拒绝执行这个事务。
    • 执行错误:在事务执行的过程中,出错的命令会被服务器识别出来,并进行相应的错误处理,所以这些出错命令不会对数据库做任何修改,也不会对事务的一致性产生任何影响。
    • 服务宕机:可以根据对应的持久化策略进行恢复,不会对事务一致性产生影响。

    总结

    事务,主要做了 控制并发故障恢复 两件事。redis 事务与传统的关系型数据库事务有些许不同:

    • 原子性:redis 不支持事务回滚机制。即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止
    • 持久性:redis 事务的持久性取决于 redis 自身选择的持久化方式,生产上我们基本不会选择效率低下的 appendfsync = everysec 模式,因此,一般不具备持久性

    除此之外,redis 事务是支持隔离性一致性。

    redis 作者在其文章中也提到,redis lua 脚本似乎正在取代 redis 事务,毕竟后起之秀 ---- redis lua 脚本远比 redis 事务应用广泛(事务在redis lua 脚本之前出现)。

    大势所趋,也许不久的将来 redis 事务相关代码将会被下掉。




    相关参考:
  • 相关阅读:
    Web自动化成长之路:selenium中三种等待方式/三大切换操作
    springBoot配置多数据源
    Python3出现的Error总结
    echarts的bug,在series里写tooltip,不起作用,要在全局先写tooltip:{}才起作用,如果在series里写的不起作用就写到全局里
    HTML---基础入门知识详解
    Roson的Qt之旅#105 QML Image引用大尺寸图片
    maven的坐标元素
    HashMap 源码分析
    python LeetCode 刷题记录 27
    在非金融应用中在哪里使用区块链?
  • 原文地址:https://blog.csdn.net/ldw201510803006/article/details/126093450