事务属性
事务是对数据库进行读写的一系列操作。在事务执行时提供ACID属性保证: 包括原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
- 原子性(Atomicity): 事务中多个操作要么全部成功,要么全部失败。
- 一致性(Consistency): 数据库中的数据在事务执行前后是一致的。
- 隔离性(Isolation): 一个事务的执行不被其他事务影响。
- 持久性(Durability):数据库执行事务后,数据的修改被持久化保存。
Redis 中的事务如何工作:
- Redis 事务允许在一个步骤中执行一组命令,Redis提供了事务相关的命令: MULTI 、 EXEC、 DISCARD、WATCH。
- Redis 事务做了两个重要的保证:
1)事务中的所有命令都被序列化并按顺序执行。另一个客户端发送的请求永远不会在执行Redis事务的过程中被服务。这保证了命令作为单个隔离操作执行。
2)EXEC命令触发事务中所有命令的执行,因此,如果客户机在调用EXEC命令之前在事务的上下文中失去与服务器的连接,则不会执行任何操作,相反,如果调用EXEC命令,则执行所有操作。
原子性(Atomicity)
Redis事务机制是否能够保证原子性? 在事务期间,可能会遇到两种命令错误:
- 命令可能无法排队,因此在调用EXEC之前可能会出现错误。例如,该命令可能在语法上是错误的(参数数量错误,命令名称错误,…),或者使用不存在的命令,或者可能存在一些关键条件,如内存不足条件(如果服务器使用maxmemory指令配置了内存限制)。
# 开启事务
192.168.88.11:6380> multi
OK
# 语法错误,redis不支持该命令
192.168.88.11:6380> sett key hello
(error) ERR unknown command `sett`, with args beginning with: `key`, `hello`,
# 正确命令,Redis命令入队
192.168.88.11:6380> incr counter
QUEUED
# 执行事务,因之前命令有错,事务无法执行
192.168.88.11:6380> exec
(error) EXECABORT Transaction discarded because of previous errors.
# 键counter为空
192.168.88.11:6380> get counter
(nil)
- 在调用EXEC之后,命令可能会失败,例如我们对具有错误值的键执行了操作(例如对字符串值调用列表操作)。即命令和操作的数据类型不匹配。但Redis实例并没有检查出错误。这种属于运行时错误,Redis在执行这些事务操作时就会报错。
需要注意的是, 即使命令失败,队列中的所有其他命令也会被处理——Redis 不会停止命令的处理。
需要注意的是, 即使命令失败,队列中的所有其他命令也会被处理——Redis 不会停止命令的处理。
需要注意的是, 即使命令失败,队列中的所有其他命令也会被处理——Redis 不会停止命令的处理。
# 键 k1 为字符串类型
192.168.88.11:6380> type k1
string
# k1 当前值为 hello
192.168.88.11:6380> get k1
"hello"
# 开启事务
192.168.88.11:6380> multi
OK
# 对 k1 字符串类型调用列表操作: LPUSH,此时并不报错
192.168.88.11:6380> LPUSH k1 1
QUEUED
# 继续执行append操作
192.168.88.11:6380> APPEND k1 world
QUEUED
# 执行事务,第一个操作报错,第二个操作正常执行
192.168.88.11:6380> exec
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) (integer) 10
# k1 的傎已修改
192.168.88.11:6380> get k1
"helloworld"
Redis 不支持事务回滚,因为支持回滚会对 Redis 的简单性和性能产生重大影响。虽然 Redis 提供了 DISCARD 命令,DISCARD可用于中止事务。丢弃命令队列。 此时,不会执行任何命令,连接状态恢复正常。起不到回滚的效果。
DISCARD可用于中止事务。丢弃命令队列。不是回滚命令。
DISCARD可用于中止事务。丢弃命令队列。不是回滚命令。
DISCARD可用于中止事务。丢弃命令队列。不是回滚命令。
# 设置foo的值为1
192.168.88.11:6380> SET foo 1
OK
# 开启事务
192.168.88.11:6380> MULTI
OK
# 自增操作
192.168.88.11:6380> INCR foo
QUEUED
# 执行DISCARD 命令,放弃事务
192.168.88.11:6380> DISCARD
OK
# 再次读取foo值,值没有被修改
192.168.88.11:6380> GET foo
"1"
- 如果在执行事务 EXEC 命令时,Redis实例或机器意外故障。导致事务执行失败。
1)如果没有开启AOF,操作日志不会被记录,数据丢弃。
2)如果有开启AOF,AOF 日志是仅追加日志,断电时也不会出现损坏问题。即使由于某种原因(磁盘已满或其他原因)日志以半写命令结束,redis-check-aof 工具也能够轻松修复它。
如果 AOF 被截断,该怎么办?
当 aof-load-truncated 启用时,AOF 中的最后一个命令可能会被截断。Redis 的最新主要版本无论如何都能够加载 AOF,只是丢弃文件中最后一个格式不正确的命令。在这种情况下,会有下面日志:此时事务操作不会再被执行,从而保证原子性。
2213:M 22 Feb 2024 14:40:08.204 # !!! Warning: short read while loading the AOF file !!!
2213:M 22 Feb 2024 14:40:08.204 # !!! Truncating the AOF at offset 237 !!!
2213:M 22 Feb 2024 14:40:08.204 # AOF loaded anyway because aof-load-truncated is enabled
2213:M 22 Feb 2024 14:40:08.204 * DB loaded from append only file: 0.000 seconds
当 aof-load-truncated 未启用时,Redis无法启动,会有下面的日志:
2302:M 22 Feb 2024 14:56:13.638 # Unexpected end of file reading the append only file. You can: 1) Make a backup of your AOF file, then use ./redis-check-aof --fix <filename>. 2) Alternatively you can set the 'aof-load-truncated' configuration option to yes and restart the server.
需要使用 redis-check-aof 工具修复AOF 日志文件,把未完成的事务操作从 AOF 文件中删除。通过AOF 恢复实例后,事务操作不会再被执行,保证了原子性。
执行以下步骤进行恢复:
root@ubuntu-x64_01:/data/redis/data# cp appendonly.aof appendonly.aof.bak
制作 AOF 文件的备份副本。
使用Redis附带的redis-check-aof 工具修复原始文件:
root@ubuntu-x64_01:/data/redis/data# redis-check-aof --fix appendonly.aof
0x f5: Expected to read 6 bytes, got 0 bytes
AOF analyzed: size=245, ok_up_to=166, diff=79
This will shrink the AOF from 245 bytes, with 79 bytes, to 166 bytes
Continue? [y/N]: y
Successfully truncated AOF
所以Redis对事务原子性属性不同场景下会不同。具体如下:
1)如果命令语法错误、在命令入队就报错,事务将无法执行,保证了原子性。
2)如果命令本身正确,命令入队成功。但实际执行时报错。无法保证原子性。
3)如果EXEC命令执行时实例故障。此时有开启AOF,可以保证原子性。
综合上面场景:
当命令入队时没报错,实际执行时报错,运行时错误。不能保证原子性!!!
当命令入队时没报错,实际执行时报错,运行时错误。不能保证原子性!!!
当命令入队时没报错,实际执行时报错,运行时错误。不能保证原子性!!!
一致性(Consistency)
事务的一致性要看具体场景,事务在执行时可能会有错误命令、参数数量错误、实例故障等因素影响。
1) 调用EXEC之前出现错误
即命令入队时报错,例如,该命令可能在语法上是错误的(参数数量错误,命令名称错误,…),或者使用不存在的命令。这种情况下,事务本身不会被执行,可以保证一致性。
2)调用EXEC之后出现错误
在调用EXEC之后,命令可能会失败,例如,由于我们对具有错误值的键执行了操作(例如对字符串值调用列表操作),在这种情况下,有错误的命令不会执行,正确的命令成功执行。不影响数据的一致性。
- 执行事务 EXEC 命令实例故障
如果在执行事务 EXEC 命令时,Redis实例或机器意外故障。导致事务执行失败。要根据是否持久化分场景分析:
3.1)如果没有持久化(即没开启RDB和AOF),无持久化无数据。数据是一致的。
3.2)如果有RDB持久化,事务执行时RDB快照不会执行。数据是一致的。
3.3)如果有AOF持久化,事务可能没被记录或记录不完整(使用Redis附带的redis-check-aof 工具修复原始文件),数据是一致的。
综合上面场景:
当命令语法错误、命令执行错误 或 Redis发生意外故障场景下。Redis事务机制对一致性属性有保证的。
隔离性(Isolation)
事务是在EXEC命令后才能真正执行,EXEC命令之前,命令会先进入队列。 分两种场景来分析:
1)调用EXEC之前
事务在 EXEC 命令前执行,命令操作是暂存在命令队列,并没有真正执行,此时。隔离性使用 WATCH 机制来实现保证 。
WATCH用于为 Redis 事务提供检查和设置 (CAS) 行为。
监视键是为了检测针对它们的更改。如果在执行EXEC命令之前至少修改了一个被监视的键,那么整个事务将终止,EXEC将返回一个Null应答来通知事务失败。
比如,我们要对键 foo 加 10 操作:
只有当我们有一个客户端在给定时间内执行操作时,这才会可靠地工作。如果多个客户端同时尝试增加键,就会出现竞争条件。例如,客户端A和B将读取旧值,例如:1。两个客户端都将该值增加到11,并最终设置为键的值。所以最后的值是11而不是21。
此时,我们需要使用WATCH命令来确保检测旧值没有其它客户端修改过,如果修改了,存在竞争条件,在调用WATCH和调用EXEC之间的时间内,另一个客户端修改了val的结果,则事务将失败。否则事务就能正常执行。
我们只需要重复这个操作,希望这次不会出现新的竞争。这种形式的锁定称为乐观锁定。在许多用例中,多个客户端将访问不同的键,因此不太可能发生冲突,通常不需要重复操作。
# 客户端1
192.168.88.11:6380> CLIENT ID
(integer) 13
192.168.88.11:6380> set key "java"
OK
192.168.88.11:6380> watch key
OK
192.168.88.11:6380> multi
OK
192.168.88.11:6380> append key go
QUEUED
192.168.88.11:6380> exec
(nil)
192.168.88.11:6380> get key
"javapython"
# -----------------------------------------------
# 客户端 2
192.168.88.11:6380> CLIENT ID
(integer) 14
# 此步骤在 客户1 开启事务后 执行
192.168.88.11:6380> append key python
(integer) 10
192.168.88.11:6380> get key
"javapython"
2)调用EXEC之后
因为Redis主线程是单线程执行命令,EXEC命令执行后,Redis会先执行命令队列中的所有命令执行完。再处理其它客户请求操作命令。所以这种情况不会影响事务的隔离性。
综上场景,使用WATCH命令在事务执行前,检测它们的更改。如果在执行EXEC命令之前至少修改了一个被监视的键,那么整个事务将终止,EXEC将返回一个Null应答来通知事务失败,避免事务的隔离性被破坏。如果在EXEC命令之后执行,Redis会保证先把命令队列中的所有命令执行完。不会破坏事务的隔离性。
持久性(Durability)
事务的持久性取决于Redis持久化配置,如果Redis没有配置RDB和AOF持久化,无持久化无数据。无持久性保证。那么事务属性肯定得不到保证。如果配置了持久性,根据不同的场景分析如下:
1)如果有RDB持久化,事务执行后,RDB快照还未执行就故障。无持久性保证。
2)如果有AOF持久化,取决于三种配置(no、everysec、alway)都存在数据丢失的情况,无持久性保证。
综上,不管是哪种模式,持久性都无法保证。 Redis本身是内存数据库,持久性并不是必须的属性。
小结
1)Redis事务ACID属性可以保证一致性和隔离性。但是无法保证原子性、持久性(由于Redis本身是内存数据库),持久性并不是必须的属性。一般关注ACI属性。
2)Redis的原子性在使用时较复杂,请参考官方文档操作命令,要确保命令运行正确,否则原子性无法得到保证。
3)在使用事务时,可以结合 pileline 一次发出多个命令而无需等待每个命令的响应来提高性能、或使用LUA脚本。
4)事务还需要考虑其它的,比如redis中的操作是redis脚本,它是事务性的。Redis 事务做的所有事情,你也可以用脚本来做,而且通常脚本会更简单、更快。