事务
Redis 中的事务工作原理
Redis 事务允许以单个步骤执行一组命令,它们以 MULTI
、EXEC
、DISCARD
和 WATCH
命令为中心。Redis 事务提供两项重要保证
-
事务中的所有命令都将序列化并按顺序执行。另一个客户端发送的请求永远不会在 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
命令 将返回一个 空回复,以通知事务失败。
例如,假设我们需要以原子方式将键的值递增 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
使用上面的代码,如果存在竞争条件,另一个客户端在我们在 WATCH
命令 调用和 EXEC
命令 调用之间修改了 val
的结果,则事务将失败。
我们只需要重复该操作,希望这次我们不会遇到新的竞争。这种锁定形式称为乐观锁定。在许多用例中,多个客户端将访问不同的键,因此冲突不太可能发生 - 通常无需重复操作。
WATCH 说明
那么 WATCH
命令 到底是什么呢?它是一个命令,它将使 EXEC
命令 有条件地执行:我们要求 Redis 仅在所有 WATCH
命令 的键都没有被修改的情况下执行事务。这包括客户端进行的修改(如写入命令)以及 Redis 本身进行的修改(如过期或驱逐)。如果在监控键和接收到 EXEC
命令 之间修改了键,则整个事务将中止,而不是执行。
注意
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
如果 EXEC
命令 失败(即返回 空回复),我们只需重复该操作。
Redis 脚本和事务
在 Redis 中,对于事务式操作,还需要考虑的是 Redis 脚本,它们是事务性的。您可以使用 Redis 事务完成的所有操作,也可以使用脚本完成,并且脚本通常更简单、更快。