事务

Redis 事务的工作原理

Redis 事务允许以单个步骤执行一组命令,它们围绕 MULTIEXECDISCARDWATCH 命令展开。Redis 事务提供两个重要的保证

  • 事务中的所有命令都已序列化并按顺序执行。另一个客户端发送的请求绝不会在 Redis 事务执行的中间被服务。这保证了命令作为单个独立的操作执行。

  • EXEC 命令触发事务中所有命令的执行,因此如果客户端在调用 EXEC 命令之前在事务上下文中与服务器失去连接,则不会执行任何操作;相反,如果调用了 EXEC 命令,则会执行所有操作。使用 append-only file 时,Redis 确保使用单个 write(2) 系统调用将事务写入磁盘。然而,如果 Redis 服务器崩溃或被系统管理员以某种强硬方式终止,则可能只有部分操作被记录下来。Redis 在重新启动时会检测到这种情况,并会伴随错误退出。使用 redis-check-aof 工具可以修复 append only file,该工具将删除部分事务,以便服务器可以再次启动。

从版本 2.2 开始,Redis 允许在上述两个保证之外提供额外的保证,采用乐观锁的形式,其方式与 check-and-set (CAS) 操作非常相似。本文档稍后对此进行说明。

用法

使用 MULTI 命令进入 Redis 事务。该命令始终回复 OK。此时,用户可以发出多个命令。Redis 不会立即执行这些命令,而是将它们排队。一旦调用 EXEC,所有命令就会执行。

改为调用 DISCARD 命令将清空事务队列并退出事务。

以下示例以原子方式对键 foobar 进行自增。

> 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 返回一个包含两个元素的 bulk string reply,其中一个为 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"

使用 check-and-set 的乐观锁

WATCH 命令用于为 Redis 事务提供 check-and-set (CAS) 行为。

WATCH 的键受到监视,以检测对其的更改。如果在 EXEC 命令之前至少有一个被监视的键被修改,则整个事务将中止,并且 EXEC 返回一个 Null reply 通知事务失败。

例如,假设我们需要以原子方式将键的值自增 1(假设 Redis 没有 INCR 命令)。

第一次尝试可能如下所示

val = GET mykey
val = val + 1
SET mykey $val

只有在给定时间内只有一个客户端执行此操作时,这才能可靠地工作。如果多个客户端几乎同时尝试自增键,就会出现竞争条件。例如,客户端 A 和 B 将读取旧值,例如 10。两个客户端都将该值自增到 11,最后将 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 本身进行的修改(例如过期或逐出)。如果在被 WATCH 的时间和接收到 EXEC 的时间之间键被修改了,则整个事务将中止。

注意

  • 在 Redis 6.0.9 之前的版本中,过期的键不会导致事务中止。更多信息
  • 事务中的命令不会触发 WATCH 条件,因为它们只会在发送 EXEC 之前排队。

WATCH 命令可以多次调用。所有 WATCH 调用都会监控从调用时刻到 EXEC 调用时刻之间的变化。您还可以向单个 WATCH 调用发送任意数量的键。

当调用 EXEC 时,所有键都会被 UNWATCH,无论事务是否中止。此外,当客户端连接关闭时,所有键都会被 UNWATCH

也可以使用 UNWATCH 命令(不带参数)来清空所有被监视的键。有时这很有用,因为我们可能会乐观地锁定一些键,可能需要执行事务来更改这些键,但在读取键的当前内容后,我们不想继续。发生这种情况时,我们只需调用 UNWATCH,以便连接可以自由地用于新的事务。

使用 WATCH 实现 ZPOP

一个很好的例子来说明如何使用 WATCH 创建 Redis 本身不支持的新原子操作,即实现 ZPOP (ZPOPMINZPOPMAX 及其阻塞变体仅在版本 5.0 中添加),ZPOP 是一个以原子方式从有序集合中弹出分数最低的元素的命令。这是最简单的实现:

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC

如果 EXEC 失败(即返回一个 Null reply),我们只需重复操作。

Redis 脚本和事务

在 Redis 中,对于类似事务的操作,还需要考虑 Redis 脚本,它们是事务性的。使用 Redis 事务可以完成的所有事情,也可以使用脚本来完成,通常脚本更简洁、更快。

给此页面评分
返回顶部 ↑