Redis 只是提供了简单的事务功能。其本质是一组命令的集合,事务支持一次执行多个命令,在事务执行过程中,会顺序执行队列中的命令,其他客户端提交的命令请求不会插入到本事务执行命令序列中。命令的执行过程是顺序执行的,但不能保证原子性。无法像 MySQL 那样,有隔离级别,出了问题之后还能回滚数据等高级操作。后面会详细分析。
Redis 提供了如下几个事务相关的基础指令。
MULTI
开启事务,Redis 会将后续命令加到队列中,而不真正执行它们,直到后续使用 EXEC
来原子化的顺序执行这些命令EXEC
执行所有事务块内的命令DISCARD
取消事务,放弃执行事务块内所有的命令WATCH
监视一个或多个 key,若事务在执行前,这些 key 被其他命令修改,则事务被终端,不会执行事务中的任何命令UNWATCH
取消 WATCH
命令对所有 keys 的监视一般情况下,一个简单的 Redis 事务主要分为如下几个部分:
MULTI
开启一个事务。QUEUED
消息。EXEC
提交事务,Redis 会依次执行队列中的命令,并依次返回所有命令的结果。(若想放弃提交事务,则执行 DISCARD
)。下图简单介绍了下 Redis 事务执行的过程:
下面我们来通过一些实际具体例子,来体会下 Redis 中的事务。前面我们也说到 Redis 的事务不是正真的事务,是无法完全满足标准事务的 ACID
特性的。通过下面的例子,我们来看看,Redis 的“破产版”事务到底存在什么问题。
[A]正常执行提交
1 | 127.0.0.1:6379> MULTI |
开启事务后,提交的命令都会加入队列(QUEUED),执行 EXEC 后会逐步执行命令并返回结果。这个看起来是不是和我们平时使用 MySQL 的事务操作相似,类似 start transaction 和 commit。
[B]正常取消事务
1 | 127.0.0.1:6379> MULTI |
开启事务后,若不想继续事务,使用 DISCARD 取消,前面提交的命令并不会真正执行,相关的 key 值不变。这个看起来也和 MySQL 的事务相似,类似 start transaction 和 rollback。
[C]WATCH 监视 key
1 | -- 线程 1 中执行 |
在开启事务之前 WATCH 了 a 的值,随后再开启事务。在另一个线程中设置了 a 的值(SET a 2),然后再 EXEC 执行事务,结果为 nil,
说明事务没有被执行。因为 a 的值在 WATCH 之后发生了变化,所以事务被取消了。
需要注意的是,这里和开启事务的时间点没有关系,与 MULTI 和另一个线程设置 a 的值的先后没有关系。只要是在 WATCH 之后发生了变化。无论事务是否已经开启,执行事务(EXEC)的时候都会取消。
普通情况下,在执行 EXEC 和 DISCARD 命令时,都会默认执行 UNWATCH。
[D]语法错误
1 | 127.0.0.1:6379> SET a 1 |
当 Redis 开启一个事务后,若添加的命令中有语法错误,会导致事务提交失败。这种情况下事务队列中的命令都不会被执行。如上面例子中 a 和 b 的值都是原有的值。
这类在 EXEC 之前产生的错误,如命令名称错误,命令参数错误等,会在 EXEC 执行之前被检测出来,所以在发生这些错误的时候,事务会被取消,事务中的所有命令都不会执行。(这种情况看起来是不是有点像回滚了)
[E]运行时错误
1 | 127.0.0.1:6379> MULTI |
当 Redis 开启一个事务后,添加的命令没有出现前面说的语法错误,但是在运行时检测到了类型错误,导致事务最提交失败(说未完全成功可能更准确点)。此时事务并不会回滚,而是跳过错误命令继续执行。
如上面的例子,未报错的命令值已经修改,a 被设置成了 1,b 被设置为了 hello,但是报错的值未被修改,即 INCR b 类型错误,并未执行,b 的值也没有被再更新。
通过上面的例子,我们已经知道 Redis 的事务和我们通常接触的 MySQL 等关系数据库的事务还有有一定差异的。它不保证原子性。同时 Redis 事务也没有事务隔离级别的概念。下面我们来具体看下 Redis 在 ACID 四个特性中,那些是满足的,那些是不满足的。
事务执行可以分为命令入队(EXEC 执行前)和命令实际执行(EXEC 执行之后)两个阶段。下面我们在分析的时候,很多时候都会分这两种情况来分析。
上面的实例分析中,[A],[B],[C]三种正常的情况,我们可以很明显的看出,是 保证了原子性的 。
但是一些异常情况下,是不满足原子性的。
redis-check-aof
工具检测 AOF 日志文件,可以把未完成的事务操作从 AOF 文件中去除。这样一来,使用 AOF 文件恢复实例后,事务操作不会被再执行,从而 保证了原子性 。若使用的 RDB 模式,最新的 RDB 快照是在 EXEC 执行之前生成的,使用快照恢复之后,事务中的命令也都没有执行,从而 保证了原子性。若 Redis 没有开启持久化,则重启后内存中的数据全部丢失,也就谈不上原子性了。一致性指的是事务执行前后,数据符合数据库的定义和要求。这点在 Redis 事务中是满足的,不论是发生语法错误还是运行时错误,错误的命令均不会被执行。
事务会放弃执行,故可以保证一致性。
错误的命令不会被执行,正确的命令被执行,一致性可以保证。
若 Redis 没有开启持久化,实例宕机重启后,数据都没有了,数据是一致的。
若配置了 RDB 方式,RDB 快照不会在事务执行时执行。所以,若事务执行到一半,实例发生了故障,此时上一次 RDB 快照中不会包含事务所做的修改,而下一次 RDB 快照还没有执行,实例重启后,事务修改的数据会丢失,数据是一致的。若事务已经完成,但新一次的 RDB 快照还没有生成,那事务修改的数据也会丢失,数据也是一致的。
若配置了 AOF 方式。当事务操作还没被记录到 AOF 日志时,实例就发生故障了,使用 AOF 日志恢复后数据是一致的。若事务中的只有部分操作被记录到 AOF 日志,可以使用 redis-check-aof
清除事务中已经完成的操作,数据库恢复后数据也是一致的。
情况 a 可以参考前面的实例分析 WATCH 命令的使用。
情况 b,由于 Redis 是单线程执行命令,EXEC 命令执行后,Redis 会保证先把事务队列中的所有命令执行完之后再执行之后的命令。
若 Redis 没有开启持久化,那么就是所有数据都存储在内存中,一旦重启,数据就会丢失,因此此时事务的持久性是肯定无法得到保证的。
若 Redis 开启了持久化,当实例宕机重启,还是会有可能丢失数据,因此也并能完全保证持久性。
因此,我们可以说 Redis 事务无法一定保证持久性,仅在特殊的情况下,可以保证持久性。
关于 Redis 在开启持久化之后,为啥还会丢失数据,笔者会单独整理一篇 Redis 持久化与主从相关的文章来介绍,此处简单说下。
如果配置了 RDB 模式,在一个事务执行后,下一次 RDB 快照还未执行前,Redis 实例发生了宕机,数据就会丢失、
如果配置了 AOF 模式,而 AOF 模式的三种配置选项 no,everysec,always 也都可能会产生数据丢失的情况。
总结一下,Redis 事务对 ACID 的支持情况:
看一下官网的的说明:
What about rollbacks?
Redis does not support rollbacks of transactions since supporting rollbacks would have a significant impact on the simplicity and performance of Redis.
大部分需要事务回滚的情况是程序错误导致的,这种情况一般是开发环境,生产环境不应该出现这种错误。
对于逻辑错误,例如应该加 1,结果写成了加 2,这种情况无法通过回滚来解决。
Redis 追求的是简单高效,而传统事务的实现相对复杂很多,这和 Redis 的设计思想是违背的。当我们享受 Redis 的快速时,也就无法再要求它更多。
本文主要介绍了 Redis 事务的基础指令与执行流程,并分析了其对传统 ACID 特性支持的情况,相信大家对 Redis 事务已经有了一个简单的了解。
通过上面的介绍,会发现 Redis 的事务似乎有点鸡肋,确实实际中也很少会使用。至于事务的具体实现,笔者后续文章会结合源码进行分析。今天的文章就到这里,下期我们接着学。