Valkey 事务允许一次性执行一组命令,它们围绕着 MULTI
、EXEC
、DISCARD
和 WATCH
命令。Valkey 事务提供两个重要保证:
-
事务中的所有命令都是序列化的并按顺序执行。其他客户端发送的请求绝不会在 Valkey 事务执行的**中间**被处理。这保证了命令作为单个隔离操作执行。
-
EXEC
命令触发事务中所有命令的执行,因此如果客户端在调用EXEC
命令之前在事务上下文中丢失了与服务器的连接,则不会执行任何操作;反之,如果调用了EXEC
命令,则会执行所有操作。当使用 AOF 持久化文件时,Valkey 确保使用单个 write(2) 系统调用将事务写入磁盘。但是,如果 Valkey 服务器崩溃或被系统管理员以某种强硬方式终止,则可能只记录了部分操作。Valkey 将在重启时检测到此情况,并会以错误退出。使用valkey-check-aof
工具可以修复 AOF 持久化文件,它将移除部分事务,以便服务器可以再次启动。
Valkey 在上述两个保证之外,还提供了一种额外的保证,即以一种与检查并设置 (CAS) 操作非常相似的方式实现乐观锁。这将在本页的后面进行说明。
用法
Valkey 事务通过 MULTI
命令进入。该命令总是回复 OK
。此时用户可以发出多个命令。Valkey 不会立即执行这些命令,而是会将它们排队。所有命令都将在调用 EXEC
后执行。
而调用 DISCARD
将清空事务队列并退出事务。
以下示例原子地递增键 foo
和 bar
。
> 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 事务完成的事情,也可以使用脚本完成。