Redis-事务
原文:CSDN
所谓的事务,就是指对数据进行读写的一系列操作。事务在执行时,会提供专门的属性保证,包括原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),也就是 ACID 属性。Redis 可以完全保证 ACID 属性吗?毕竟,如果有些属性在一些场景下不能保证的话,很可能会导致数据出错,所以,我们必须要掌握 Redis 对这些属性的支持情况,并且提前准备应对策略。
Redis实现步骤
Section titled “Redis实现步骤”事务的执行分为三个步骤,且用MULTI,EXEC这两个命令取完成这三个步骤:
- MULTI:客户端使用一个命令显式地表示一个事务的开启。
- 客户端把事务本身要执行的操作(CRUD)发送给服务器端。这些命令暂存到一个命令队列中,并不是立即执行。
- EXEC:客户端向服务端发送提交事务的命令,让数据库实际执行第二步发送具体操作。下面的代码就显示了使用 MULTI 和 EXEC 执行一个事务的过程:
#开启事务127.0.0.1:6379> MULTIOK#将a:stock减1,127.0.0.1:6379> DECR a:stockQUEUED#将b:stock减1127.0.0.1:6379> DECR b:stockQUEUED#实际执行事务127.0.0.1:6379> EXEC1) (integer) 42) (integer) 9我们假设 a:stock、b:stock 两个键的初始值是 5 和 10。在 MULTI 命令后执行的两个 DECR 命令,是把 a:stock、b:stock 两个键的值分别减 1,它们执行后的返回结果都是 QUEUED,这就表示,这些操作都被暂存到了命令队列,还没有实际执行。等到执行了 EXEC 命令后,可以看到返回了 4、9,这就表明,两个 DECR 命令已经成功地执行了。
通过使用 MULTI 和 EXEC 命令,我们可以实现多个操作的共同执行,但是这符合事务要求的 ACID 属性吗?接下来,我们就来具体分析下。
Redis的事务机制能保证哪些属性
Section titled “Redis的事务机制能保证哪些属性”原子性是事务操作最重要的一个属性,所以,我们先来分析下 Redis 事务机制能否保证原子性。
如果事务正常执行,没有发生任何错误,那么,MULTI 和 EXEC 配合使用,就可以保证多个操作都完成。但是,如果事务执行发生错误了,原子性还能保证吗?我们需要分三种情况来看。
执行EXEC命令前,客户端发送的操作命令有错误
Section titled “执行EXEC命令前,客户端发送的操作命令有错误”在命令入队时,Redis 就会报错并且记录下这个错误。此时,我们还能继续提交命令操作。等到执行了 EXEC 命令之后,Redis 就会拒绝执行所有提交的命令操作,返回事务失败的结果。这样一来,事务中的所有命令都不会再被执行了,保证了原子性。
#开启事务127.0.0.1:6379> MULTIOK#发送事务中的第一个操作,但是Redis不支持该命令,返回报错信息127.0.0.1:6379> PUT a:stock 5(error) ERR unknown command `PUT`, with args beginning with: `a:stock`, `5`,#发送事务中的第二个操作,这个操作是正确的命令,Redis把该命令入队127.0.0.1:6379> DECR b:stockQUEUED#实际执行事务,但是之前命令有错误,所以Redis拒绝执行127.0.0.1:6379> EXEC(error) EXECABORT Transaction discarded because of previous errors.在这个例子中,事务里包含了一个 Redis 本身就不支持的 PUT 命令,所以,在 PUT 命令入队时,Redis 就报错了。虽然,事务里还有一个正确的 DECR 命令,但是,在最后执行 EXEC 命令后,整个事务被放弃执行了。
事务操作入队时,命令和操作的数据类型不匹配,但Redis实例没有检查出错误
Section titled “事务操作入队时,命令和操作的数据类型不匹配,但Redis实例没有检查出错误”在执行完 EXEC 命令以后,Redis 实际执行这些事务操作时,就会报错。不过,需要注意的是,虽然 Redis 会对错误命令报错,但还是会把正确的命令执行完。在这种情况下,事务的原子性就无法得到保证了。举个小例子。事务中的 LPOP 命令对 String 类型数据进行操作,入队时没有报错,但是,在 EXEC 执行时报错了。LPOP 命令本身没有执行成功,但是事务中的 DECR 命令却成功执行了。
#开启事务127.0.0.1:6379> MULTIOK#发送事务中的第一个操作,LPOP命令操作的数据类型不匹配,此时并不报错127.0.0.1:6379> LPOP a:stockQUEUED#发送事务中的第二个操作127.0.0.1:6379> DECR b:stockQUEUED#实际执行事务,事务第一个操作执行报错127.0.0.1:6379> EXEC1) (error) WRONGTYPE Operation against a key holding the wrong kind of value2) (integer) 8看到这里,你可能有个疑问,传统数据库(例如 MySQL)在执行事务时,会提供回滚机制,当事务执行发生错误时,事务中的所有操作都会撤销,已经修改的数据也会被恢复到事务执行前的状态,那么,在刚才的例子中,如果命令实际执行时报错了,是不是可以用回滚机制恢复原来的数据呢?Redis 中并没有提供回滚机制。虽然 Redis 提供了 DISCARD 命令,但是,这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。
#读取a:stock的值4127.0.0.1:6379> GET a:stock"4"#开启事务127.0.0.1:6379> MULTIOK#发送事务的第一个操作,对a:stock减1127.0.0.1:6379> DECR a:stockQUEUED#执行DISCARD命令,主动放弃事务127.0.0.1:6379> DISCARDOK#再次读取a:stock的值,值没有被修改127.0.0.1:6379> GET a:stock"4"这个例子中,a:stock 键的值一开始为 4,然后,我们执行一个事务,想对 a:stock 的值减 1。但是,在事务的最后,我们执行的是 DISCARD 命令,所以事务就被放弃了。我们再次查看 a:stock 的值,会发现仍然为 4。
执行事务的EXEC命令,Redis实例发生了故障,导致事务执行失败
Section titled “执行事务的EXEC命令,Redis实例发生了故障,导致事务执行失败”如果 Redis 开启了 AOF 日志,那么,只会有部分的事务操作被记录到 AOF 日志中。我们需要使用 redis-check-aof 工具检查 AOF 日志文件,这个工具可以把未完成的事务操作从 AOF 文件中去除。这样一来,我们使用 AOF 恢复实例后,事务操作不会再被执行,从而保证了原子性。
当然,如果 AOF 日志并没有开启,那么实例重启后,数据也都没法恢复了,此时,也就谈不上原子性了。
你了解了 Redis 对事务原子性属性的保证情况,我们来简单小结下:
- 命令入队时就报错,会放弃事务执行,保证原子性;
- 命令入队时没报错,实际执行时报错,不保证原子性;
- EXEC 命令执行时实例故障,如果开启了 AOF 日志,可以保证原子性。
事务的隔离性保证,会受到和事务一起执行的并发操作的影响。事务执行又可以分成命令入队(EXEC 命令执行前)和命令实际执行(EXEC 命令执行后)两个阶段,所以,我们就针对这两个阶段,分成两种情况来分析:
- 并发操作在EXEC命令前执行:隔离性的保证要使用WATCH机制来实现,否则隔离性无法保证;
- 并发操作在EXEC命令后执行:此时,隔离性可以保证;对于第一种情况。一个事务的EXEC命令还没有执行时,事务的命令操作是暂存在命令队列中的。此时如果有其他的并发操作,我们就需要看事务是否使用了WATCH机制。WATCH机制作用:在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了。
WATCH机制的具体实现是由WATCH命令实现的,举个例子:

在 t1 时,客户端 X 向实例发送了 WATCH 命令。实例收到 WATCH 命令后,开始监测 a:stock 的值的变化情况。紧接着,在 t2 时,客户端 X 把 MULTI 命令和 DECR 命令发送给实例,实例把 DECR 命令暂存入命令队列。在 t3 时,客户端 Y 也给实例发送了一个 DECR 命令,要修改 a:stock 的值,实例收到命令后就直接执行了。
等到 t4 时,实例收到客户端 X 发送的 EXEC 命令,但是,实例的 WATCH 机制发现 a:stock 已经被修改了,就会放弃事务执行。这样一来,事务的隔离性就可以得到保证了。当然,如果没有使用 WATCH 机制,在 EXEC 命令前执行的并发操作是会对数据进行读写的。而且,在执行 EXEC 命令的时候,事务要操作的数据已经改变了,在这种情况下,Redis 并没有做到让事务对其它操作隔离,隔离性也就没有得到保障。下面这张图显示了没有 WATCH 机制时的情况,你可以看下。

在 t2 时刻,客户端 X 发送的 EXEC 命令还没有执行,但是客户端 Y 的DECR 命令就执行了,此时,a:stock 的值会被修改,这就无法保证 X 发起的事务的隔离性了。刚刚说的是并发操作在 EXEC 命令前执行的情况,下面我再来说一说第二种情况:并发操作在 EXEC 命令之后被服务器端接收并执行。因为 Redis 是用单线程执行命令,而且,EXEC 命令执行后,Redis会保证先把命令队列中的所有命令执行完。所以,在这种情况下,并发操作不会破坏事务的隔离性,如下图所示:

最后,我们来分析一下 Redis 事务的持久性属性保证情况。
因为 Redis 是内存数据库,所以,数据是否持久化保存完全取决于 Redis 的持久化配置模式。如果 Redis 没有使用 RDB 或 AOF,那么事务的持久化属性肯定得不到保证。如果 Redis 使用了 RDB 模式,那么,在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,这种情况下,事务修改的数据也是不能保证持久化的。
如果 Redis 采用了 AOF 模式,因为 AOF 模式的三种配置选项 no、everysec 和 always 都会存在数据丢失的情况,所以,事务的持久性属性也还是得不到保证。所以,不管 Redis 采用什么持久化模式,事务的持久性属性是得不到保证的。
在这节课上,我们学习了 Redis 中的事务实现。Redis 通过 MULTI、EXEC、DISCARD 和 WATCH 四个命令来支持事务机制,这 4 个命令的作用,我总结在下面的表中,你可以再看下。

事务的 ACID 属性是我们使用事务进行正确操作的基本要求。通过这节课的分析,我们了解到了,Redis 的事务机制可以保证一致性和隔离性,但是无法保证持久性。不过,因为 Redis 本身是内存数据库,持久性并不是一个必须的属性,我们更加关注的还是原子性、一致性和隔离性这三个属性。
原子性的情况比较复杂,只有当事务中使用的命令语法有误时,原子性得不到保证,在其它情况下,事务都可以原子性执行。
所以,我给你一个小建议:严格按照 Redis 的命令规范进行程序开发,并且通过 code review 确保命令的正确性。这样一来,Redis 的事务机制就能被应用在实践中,保证多操作的正确执行。