redis 事务,深入解读

34 篇文章 3 订阅
25 篇文章 5 订阅


前言

本文参考源码版本: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"

我们继续看看,当遇到一些异常情况,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"

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

案例 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)

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


综上,可以得出结论,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"

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

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

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

typedef struct client {

    ...
    uint64_t flags;        
    multiState mstate;

    ...

}
  • flags 标志:当我们提交 multi 指令时,将会加上 CLIENT_MULTI 标志,即 事务开启
  • multiState:命令队列,即 记录所有事务中的指令

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

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

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

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

127.0.0.1:6379> discard
OK

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

接着,客户端 2 执行:

127.0.0.1:6379> set key2 other_modify_it
OK

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

127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get key1
"ok"

发现了吧,事务没执行! 由于我们在客户端 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"

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

127.0.0.1:6379> unwatch
OK

2)原理:

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

typedef struct client {

    ...
    
    uint64_t flags;    
    list *watched_keys;  // 链表结构,指向 watchedKey 类型的链表
 
    ...

}

watchedKey 结构体:

typedef struct watchedKey {
    robj *key;  // key
    redisDb *db; // 所在 db
} watchedKey;

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 事务相关代码将会被下掉。




相关参考:
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柏油

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值