交易
Redis 中的事务如何工作
Redis 事务允许在一个步骤中执行一组命令,它们以命令
、 和MULTI为EXEC中心。Redis 事务做出两个重要保证:DISCARDWATCH
-
事务中的所有命令都是序列化的,并按顺序执行。在 Redis 事务执行过程中,其他客户端发送的请求永远不会得到处理。这保证了命令作为单个隔离操作执行。
-
该
EXEC命令会触发事务中所有命令的执行,因此如果客户端在调用命令之前在事务上下文中丢失了与服务器的连接,则EXEC不会执行任何操作,相反,如果EXEC调用了命令,则会执行所有操作。使用 仅追加文件时,Redis 会确保使用单个 write(2) 系统调用将事务写入磁盘。但是,如果 Redis 服务器崩溃或被系统管理员以某种困难方式杀死,则可能只有部分操作被注册。Redis 将在重新启动时检测到这种情况,并退出并显示错误。使用该redis-check-aof工具可以修复仅追加文件,删除部分事务,以便服务器可以重新启动。
从 2.2 版开始,Redis 允许在上述两种情况的基础上提供额外的保证,即以乐观锁定的形式提供,其方式与检查并设置 (CAS) 操作非常相似。本页后面将对此进行介绍。
用法
使用命令输入 Redis 事务MULTI。命令始终以 回复OK。此时,用户可以发出多个命令。Redis 不会执行这些命令,而是将它们排队。所有命令都执行一次EXEC调用。
调用DISCARD将会刷新事务队列并退出事务。
以下示例以原子方式增加键foo和bar。
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
从上面的会话可以清楚地看出,EXEC返回一个答复数组,其中每个元素都是事务中单个命令的答复,按照发出命令的顺序排列。
当 Redis 连接处于请求上下文中时MULTI,所有命令都将使用字符串进行回复QUEUED(从 Redis 协议的角度来看,作为状态回复发送)。排队命令只是在EXEC调用时安排执行。
交易内部的错误
在交易过程中可能会遇到两种命令错误:
- 命令可能无法排队,因此在
EXEC调用之前可能会出现错误。例如,命令可能语法错误(参数数量错误、命令名称错误等),或者可能存在某些严重情况,如内存不足(如果服务器使用指令配置了内存限制maxmemory)。 - 命令在调用后
EXEC可能会失败,例如因为我们对具有错误值的键执行了操作(如对字符串值调用列表操作)。
从 Redis 2.6.5 开始,服务器将在命令累积期间检测到错误。然后它将拒绝执行事务并在期间返回错误EXEC,从而丢弃该事务。
Redis < 2.6.5 版本注意事项:在 Redis 2.6.5 之前,客户端需要通过检查排队命令的返回值来检测之前发生的错误
EXEC:如果命令回复 QUEUED,则表示已正确排队,否则 Redis 将返回错误。如果在排队命令时出现错误,大多数客户端将中止并放弃事务。否则,如果客户端选择继续执行事务,则命令EXEC将成功执行所有排队的命令,而不管之前的错误。
相反,之后 发生的错误EXEC不会以特殊方式处理:即使事务期间某些命令失败,所有其他命令仍将被执行。
这在协议层面上更加清晰。在下面的例子中,即使语法正确,一个命令在执行时也会失败:
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-WRONGTYPE Operation against a key holding the wrong kind of value
EXEC返回两个元素的批量字符串回复,其中一个是OK代码,另一个是错误回复。客户端库需要找到一种合理的方式向用户提供错误。
值得注意的是, 即使某个命令失败,队列中的所有其他命令都会被处理——Redis 不会停止命令的处理。
另一个例子,再次使用有线协议telnet,展示了如何尽快报告语法错误:
MULTI
+OK
INCR a b c
-ERR wrong number of arguments for 'incr' command
这次由于语法错误,错误的INCR命令根本没有排队。
那么回滚呢?
Redis 不支持事务回滚,因为支持回滚会对 Redis 的简单性和性能产生重大影响。
丢弃命令队列
DISCARD可用于中止事务。在这种情况下,不会执行任何命令,连接状态将恢复正常。
> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"
使用检查并设置的乐观锁定
WATCH用于为 Redis 事务提供检查并设置(CAS)行为。
WATCH已监视的键会受到监控,以检测针对它们的更改。如果在命令之前至少有一个被监视的键被修改EXEC,则整个事务将中止,并EXEC返回Null 回复以通知事务失败。
例如,假设我们需要原子地将某个键的值增加 1(假设 Redis 没有INCR)。
第一次尝试可能如下:
val = GET mykey
val = val + 1
SET mykey $val
仅当只有一个客户端在给定时间内执行操作时,此操作才会可靠地工作。如果多个客户端尝试在大约同一时间增加密钥,则会出现竞争条件。例如,客户端 A 和 B 将读取旧值,例如 10。两个客户端都会将该值增加到 11,并最终将其SET作为密钥的值。因此最终值将是 11,而不是 12。
感谢WATCH我们能够很好地模拟这个问题:
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
使用上述代码,如果存在竞争条件,并且另一个客户端val在我们调用WATCH和调用之间修改了的结果EXEC,则交易将失败。
我们只需重复操作,希望这次不会出现新的竞争。这种锁定形式称为乐观锁定。在许多用例中,多个客户端将访问不同的密钥,因此不太可能发生冲突 - 通常不需要重复操作。
WATCH 解释
那么,这WATCH到底是什么呢?这是一个EXEC条件命令:我们要求 Redis 仅在所有已编辑的键均未修改的情况下执行事务。这包括客户端所做的修改(如写入命令)以及 Redis 本身所做的修改(如过期或逐出)。如果在编辑键和收到键WATCH之间修改了键
,则整个事务将被中止。WATCHEXEC
笔记
WATCH可以多次调用。简单地说,所有WATCH调用都会产生效果,以监视从调用开始到EXEC调用时刻的变化。您还可以将任意数量的键发送到单个WATCH调用。
EXEC调用时,UNWATCH无论事务是否中止,所有键都将被编辑。此外,当客户端连接关闭时,所有内容都会被UNWATCH编辑。
还可以使用UNWATCH命令(不带参数)来刷新所有受监视的键。有时这很有用,因为我们乐观地锁定了几个键,因为我们可能需要执行事务来更改这些键,但在读取键的当前内容后我们不想继续。当发生这种情况时,我们只需调用,
UNWATCH以便连接可以自由用于新事务。
使用WATCH实现ZPOP
一个很好的例子可以说明如何WATCH使用来创建 Redis 不支持的新原子操作,即实现 ZPOP(ZPOPMIN,ZPOPMAX并且它们的阻塞变体仅在版本 5.0 中添加),这是一个以原子方式从排序集合中弹出得分较低的元素的命令。这是最简单的实现:
WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC
Redis 脚本和事务
对于 redis 中的事务类操作,还需要考虑 redis 脚本,它是事务性的。使用 Redis 事务可以做的所有事情,也可以使用脚本来做,而且通常脚本更简单、更快捷。