文档:事务

Valkey 事务允许一次性执行一组命令,它们围绕着 MULTIEXECDISCARDWATCH 命令。Valkey 事务提供两个重要保证:

  • 事务中的所有命令都是序列化的并按顺序执行。其他客户端发送的请求绝不会在 Valkey 事务执行的**中间**被处理。这保证了命令作为单个隔离操作执行。

  • EXEC 命令触发事务中所有命令的执行,因此如果客户端在调用 EXEC 命令之前在事务上下文中丢失了与服务器的连接,则不会执行任何操作;反之,如果调用了 EXEC 命令,则会执行所有操作。当使用 AOF 持久化文件时,Valkey 确保使用单个 write(2) 系统调用将事务写入磁盘。但是,如果 Valkey 服务器崩溃或被系统管理员以某种强硬方式终止,则可能只记录了部分操作。Valkey 将在重启时检测到此情况,并会以错误退出。使用 valkey-check-aof 工具可以修复 AOF 持久化文件,它将移除部分事务,以便服务器可以再次启动。

Valkey 在上述两个保证之外,还提供了一种额外的保证,即以一种与检查并设置 (CAS) 操作非常相似的方式实现乐观锁。这将在本页的后面进行说明。

用法

Valkey 事务通过 MULTI 命令进入。该命令总是回复 OK。此时用户可以发出多个命令。Valkey 不会立即执行这些命令,而是会将它们排队。所有命令都将在调用 EXEC 后执行。

而调用 DISCARD 将清空事务队列并退出事务。

以下示例原子地递增键 foobar

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

从上面的会话可以清楚地看出,EXEC 返回一个回复数组,其中每个元素都是事务中单个命令的回复,其顺序与命令发出的顺序相同。

当 Valkey 连接处于 MULTI 请求的上下文中时,所有命令都将回复字符串 QUEUED(从 Valkey 协议的角度来看是状态回复)。排队的命令只是在调用 EXEC 时被调度执行。

事务中的错误

在事务执行期间,可能会遇到两种命令错误:

  • 命令可能无法排队,因此在调用 EXEC 之前可能会出现错误。例如,命令可能存在语法错误(参数数量错误、命令名称错误等),或者可能存在一些严重情况,例如内存不足(如果服务器配置了使用 maxmemory 指令设置内存限制)。
  • 命令可能在调用 EXEC 之后失败,例如,因为我们对具有错误值的键执行了操作(例如对字符串值调用列表操作)。

服务器将在命令累积期间检测到错误。然后它将拒绝执行事务,并在 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 代码,另一个是错误回复。由客户端库负责找到一种合理的方式向用户提供错误。

重要的是要注意,**即使某个命令失败,队列中的所有其他命令也会被处理** – Valkey 不会停止命令的处理。

另一个示例,同样使用 telnet 的线协议,展示了语法错误是如何尽快报告的:

MULTI
+OK
INCR a b c
-ERR wrong number of arguments for 'incr' command
EXEC
-EXECABORT Transaction discarded because of previous errors.

这次由于语法错误,错误的 INCR 命令根本没有被排队。并且 EXEC 命令将收到 EXECABORT 错误。

EXEC 命令被处理时,服务器将检查自命令排队以来是否发生了故障转移或槽迁移。如果发生了任一事件,将根据需要返回 -MOVED-REDIRECT 错误,而不处理事务。

对于集群模式

MULTI    ==>  +OK
SET x y  ==>  +QUEUED
slot {x} is migrated to other node
EXEC     ==>  -MOVED

对于单机模式

MULTI    ==>  +OK
SET x y  ==>  +QUEUED
failover
EXEC     ==>  -REDIRECT

EXEC 命令处理之前,如果某个命令访问了不属于当前节点的数据,将立即返回 -MOVED-REDIRECT 错误,并且 EXEC 命令将收到 EXECABORT 错误。

对于集群模式

MULTI    ==>  +OK
SET x y  ==>  -MOVED
EXEC     ==>  -EXECABORT

对于单机模式

MULTI    ==>  +OK
SET x y  ==>  -REDIRECT
EXEC     ==>  -EXECABORT

关于回滚?

Valkey 不支持事务回滚,因为支持回滚将对 Valkey 的简洁性和性能产生重大影响。

丢弃命令队列

DISCARD 可用于中止事务。在这种情况下,不执行任何命令,并且连接状态恢复正常。

> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"

使用检查并设置(CAS)实现乐观锁

WATCH 用于为 Valkey 事务提供检查并设置(CAS)行为。

WATCH 的键被监控以检测其上的更改。如果在 EXEC 命令之前至少一个被监控的键被修改,则整个事务将中止,并且 EXEC 返回一个 空回复 以通知事务失败。

例如,假设我们需要原子地将一个键的值增加 1(我们假设 Valkey 没有 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 条件化的命令:我们要求 Valkey 仅在没有 WATCH 的键被修改的情况下才执行事务。这包括客户端进行的修改(如写入命令)以及 Valkey 自身进行的修改(如过期或逐出)。如果键在被 WATCH 和收到 EXEC 之间被修改,则整个事务将被中止。

**注意:** 事务中的命令不会触发 WATCH 条件,因为它们只会被排队,直到 EXEC 命令发送。

WATCH 可以多次调用。简单来说,所有 WATCH 调用都会从调用开始,直到 EXEC 被调用那一刻,持续监控更改。您也可以在一个 WATCH 调用中发送任意数量的键。

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

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

使用 WATCH 实现 ZPOPMIN

一个说明如何使用 WATCH 创建原子操作的例子是实现 ZPOPMIN,这是一个以原子方式从有序集合中弹出分数最低元素的命令。这是一个可能的实现:

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

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

Valkey 脚本和事务

对于类似事务的操作,另一个要考虑的是脚本,它们是事务性的。所有您可以使用 Valkey 事务完成的事情,也可以使用脚本完成。