本文参考源码版本:
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 来进一步扩展事务的能力:
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 事务使用:
在服务端的 client 结构体中,有如下定义:
typedef struct client {
...
uint64_t flags;
multiState mstate;
...
}
服务端处理流程上大致是这样:
从实现上来看,redis 的事务相关逻辑十分简单,当然,这也是为了性能,牺牲了部分原子性保障。
可以开启事务,自然也会有配套的取消事务
,通过指令 discard 来完成:
127.0.0.1:6379> discard
OK
当多个客户端对相同的 key 并发做修改时,可能会出现不可预期的结果;我们通常做法时,在应用层全局加锁处理,当然,这这种叫悲观锁
。
redis 在事务中提供了更加高效的乐观锁
机制,通过 WATCH
、UNWATCH
指令可以监听或取消监听待处理的 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 标志,如果有,则直接取消事务,当然,也就不会执行任何指令。
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 lua 脚本似乎正在取代 redis 事务
,毕竟后起之秀 ---- redis lua 脚本远比 redis 事务应用广泛(事务在redis lua 脚本之前出现)。
大势所趋,也许不久的将来 redis 事务相关代码将会被下掉。
- 事务(transaction)- 翻译版
- Transactions - Redis
- <
>「黄健宏」 - <
>「陈雷」