MULTI,EXEC,DISCARD和WATCH是Redis交易的基础。它们允许在一个步骤中执行一组命令,具有两个重要保证:
事务中的所有命令都被序列化并按顺序执行。在执行Redis事务的过程中,不会发生由另一个客户端发出的请求。这可以保证命令作为单个隔离操作执行。
要么处理所有命令,要么都不处理,因此Redis事务也是原子的。 EXEC命令触发事务中所有命令的执行,因此如果客户端在调用MULTI命令之前在事务上下文中丢失了与服务器的连接,则不执行任何操作,而如果调用EXEC命令,则执行所有操作。使用 仅追加文件 时,Redis确保使用单个write(2)系统调用在磁盘上写入事务。但是,如果Redis服务器以某种困难的方式崩溃或被系统管理员杀死,则可能只注册了部分操作。 Redis将在重新启动时检测到此情况,并将退出并显示错误。使用redis-check-aof工具可以修复仅删除部分事务的append only文件,以便服务器可以重新启动。
从版本2.2开始,Redis允许对上述两者进行额外保证,其形式为 乐观锁定,其方式与check and set(CAS)操作非常相似。这将在本页后面介绍。
用法
使用MULTI命令输入Redis事务。 该命令总是回复OK。 此时,用户可以发出多个命令。 Redis不会执行这些命令,而是将它们排队。 调用EXEC后执行所有命令。
而调用DISCARD将刷新事务队列并退出事务。
以下示例以原子方式递增键foo和bar。
1 | > MULTI |
从上面的会话中可以看出,EXEC返回一个回复数组,其中每个元素都是事务中单个命令的回复,其顺序与发出命令的顺序相同。
当Redis连接在MULTI请求的上下文中时,所有命令都将使用字符串QUEUED进行回复(从Redis协议的角度发送为状态回复)。 排队命令只是在调用EXEC时被调度执行。
事务中的错误
在事务期间,可能会遇到两种命令错误:
- 命令可能无法排队,因此在调用EXEC之前可能会出错。 例如,命令可能在语法错误(错误的参数数量,错误的命令名称,……),或者可能存在一些关键条件,如内存不足情况(如果服务器配置为使用maxmemory具有内存限制) 指示)。
- 调用EXEC后命令可能会失败,例如,因为我们对具有错误值的键执行了操作(比如对字符串值调用列表操作)。
客户端通过检查排队命令的返回值来感知在EXEC调用之前发生的第一类错误:如果命令回复QUEUED,则它正确排队,否则Redis会返回错误。 如果在排队命令时出错,大多数客户端将中止丢弃该事务的事务。
但是从Redis 2.6.5开始,服务器会记住在命令累积期间出错,并且拒绝执行事务,在EXEC期间返回错误,并自动丢弃事务。
在Redis 2.6.5之前,行为是仅使用成功排队的命令子集来执行事务,以防客户端调用EXEC而不管以前的错误。 新行为使得使用流水线操作混合事务变得更加简单,因此可以立即发送整个事务,稍后立即读取所有回复。
EXEC之后发生的错误不是以特殊方式处理的:即使某些命令在事务期间失败,所有其他命令也将被执行。
在协议级别上更清楚。 在以下示例中,即使语法正确,执行时一个命令也会失败:
1 | Trying 127.0.0.1... |
EXEC返回两元素批量字符串回复,其中一个是OK代码,另一个是-ERR回复。 由客户端库来查找向用户提供错误的合理方法。
重要的是要注意,即使命令失败,队列中的所有其他命令也会被处理 - Redis不会停止处理命令。
再一次使用telnet的有线协议的另一个例子显示了ASAP如何报告语法错误:
1 | MULTI |
这次由于语法错误,错误的INCR命令根本没有排队。
为什么Redis不支持回滚?
如果您有关系数据库背景,Redis命令在事务期间可能会失败,但仍然Redis将执行事务的其余部分而不是回滚,这可能看起来很奇怪。
但是对于这种行为有很好的意见:
- 只有在使用错误的语法调用时才会失败Redis命令(并且在命令排队期间无法检测到问题),或者对于持有错误数据类型的键,Redis命令可能会失败:这意味着实际上失败的命令是编程错误的结果, 以及在开发过程中很可能检测到的一种错误,而不是在生产中。
- Redis内部简化且速度更快,因为它不需要回滚的能力。
反对Redis观点的论点是错误发生,但应该注意的是,通常回滚不会使您免于编程错误。 例如,如果查询将键增加2而不是1,或者增加错误的键,则回滚机制无法提供帮助。 鉴于没有人可以将程序员从他或她的错误中解救出来,并且Redis命令失败所需的错误类型不太可能进入生产阶段,我们选择了更简单,更快速的方法,即不支持错误回滚。
丢弃命令队列
DISCARD可用于中止交易。 在这种情况下,不执行任何命令,并且连接状态恢复正常。
1 | > SET foo 1 |
使用CAS乐观锁
WATCH用于为Redis事务提供检查和设置(CAS)行为。
监视WATCHed键 以检测对它们的更改。 如果在EXEC命令之前修改了至少一个监视key,则整个事务将中止,并且EXEC返回Null应答以通知该事务失败。
例如,假设我们需要将键的值原子递增1(假设Redis没有INCR)。
第一次尝试可能如下:
1 | val = GET mykey |
只有当我们有一个客户在给定时间内执行操作时,这才能可靠地工作。 如果多个客户端几乎在同一时间尝试递增key,则会出现竞争条件。 例如,客户端A和B将读取旧值,例如,10。两个客户端的值将增加到11,最后SET key的值。 所以最终值将是11而不是12。
感谢WATCH,我们能够非常好地模拟问题:
1 | WATCH mykey |
使用上面的代码,如果存在竞争条件而另一个客户端在我们调用WATCH和调用EXEC之间的时间内修改了val的结果,则事务将失败。
我们只需重复操作,希望这次我们不会再遇到新的竞争。 这种形式的锁称为乐观锁,是一种非常强大的锁形式。 在许多用例中,多个客户端将访问不同的key,因此不太可能发生冲突 - 通常不需要重复操作。
WATCH解释
那么WATCH究竟是什么呢? 这是一个使EXEC成为条件的命令:只有在没有修改任何WATCHed键时,我们才会要求Redis执行事务,(但是它们可能会被交易中的同一客户更改而不会中止)否则交易根本不会输入。 (请注意,如果你观察一个易失性key并且Redis在你看完key后使key到期,那么EXEC仍然有用)。
可以多次调用WATCH。 简单地说,所有WATCH呼叫都会有效监视从呼叫开始的更改,直到调用EXEC。 您还可以向单个WATCH呼叫发送任意数量的key。
调用EXEC时,无论事务是否中止,所有键都是UNWATCHed。 此外,当客户端连接关闭时,所有内容都会被UNWATCHed。
也可以使用UNWATCH命令(不带参数)来刷新 所有 被监视的键。 有时这很有用,因为我们乐观地锁定了几个键,因为我们可能需要执行一个事务来改变这些键,但是在读完键的当前内容后我们不想继续。 当发生这种情况时,我们只需调用UNWATCH,以便连接可以自由地用于新事务。
使用WATCH实现ZPOP
一个很好的例子来说明如何使用WATCH创建新的原子操作,因为Redis不支持ZPOP,这个操作以原子方式从排序集(sorted set)中弹出具有较低分数的元素。 这是最简单的实现:
1 | WATCH zset |
如果EXEC失败(即返回Null回复),我们只需重复操作。
Redis脚本和事务
根据定义,Redis脚本是事务性的,因此您可以使用Redis事务执行所有操作,也可以使用脚本执行操作,通常脚本更简单,更快速。
这种重复是因为在Redis 2.6中引入了脚本,而事务早就存在了。 但是,我们不太可能在短时间内删除对事务的支持,因为即使不使用Redis脚本,它仍然可以避免竞争条件,特别是因为Redis事务的实现复杂性很小,因此在语义上似乎是合适的。
然而,在不久的将来,我们将看到整个用户群只是使用脚本并非不可能。 如果发生这种情况,我们可能会弃用并最终删除交易。