事务
提示
来自deepseek解释
原文链接:https://redis.io/docs/latest/develop/using-commands/transactions/
Redis 事务允许将一组命令在单步中执行,它们围绕 MULTI、EXEC、DISCARD 和 WATCH 命令展开。Redis 事务提供两个重要保证:
事务中的所有命令都会被序列化并按顺序执行。在 Redis 事务执行期间,另一个客户端发送的请求永远不会被处理。这保证了命令作为一个单独的隔离操作执行。
EXEC命令触发事务中所有命令的执行,因此如果在调用EXEC命令之前客户端在与服务器的连接上下文中断开了连接,则不会执行任何操作;相反,如果调用了EXEC命令,则所有操作都会执行。当使用仅追加文件时,Redis 确保使用一次write(2)系统调用来将事务写入磁盘。然而,如果 Redis 服务器崩溃或被系统管理员以某种强硬方式杀死,则可能只有部分操作被记录。Redis 将在重启时检测到这种情况,并退出并报错。使用redis-check-aof工具可以修复仅追加文件,该工具将移除部分事务,以便服务器能够重新启动。
从版本 2.2 开始,Redis 允许对上述两点提供额外保证,采用乐观锁定的方式,非常类似于检查-设置(CAS)操作。这将在本页后面进行说明。
用法
使用 MULTI 命令可以进入 Redis 事务。该命令始终回复 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 valueEXEC 返回了一个包含两个元素的批量字符串回复,其中一个是 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使用上述代码,如果存在竞争条件,并且在我们的 WATCH 调用和 EXEC 调用之间另一个客户端修改了 val 的结果,事务将失败。
我们只需重试该操作,希望这次不会遇到新的竞争。这种锁定形式称为乐观锁定。在许多使用场景中,多个客户端会访问不同的键,因此冲突不太可能发生——通常不需要重试操作。
从版本 8.4 开始,Redis 为字符串键提供了新的原子比较-设置和比较-删除命令。客户端可以使用单个 SET 命令(带有 IFEQ/IFNE/IFDEQ/IFDNE 选项)来原子性地更新字符串键,但仅当其值在获取后未被更改时才执行。这更简单、更快。类似地,客户端可以使用单个 DELEX 命令进行比较-删除:原子性地删除字符串键,但仅当其值在获取后未被更改时才执行。
WATCH 详解
那么 WATCH 到底是怎么回事?它是一个使 EXEC 成为条件性的命令:我们要求 Redis 仅当没有任何被 WATCH 的键被修改时才执行事务。这包括客户端所做的修改(如写命令)以及 Redis 自身所做的修改(如过期或驱逐)。如果在键被 WATCH 之后和接收到 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 失败(即返回 Null 回复),我们只需重复该操作。
Redis 脚本与事务
在 Redis 中,对于类似事务的操作,还需要考虑的是 Redis 脚本,它们也是事务性的。您可以使用 Redis 事务做的任何事情,也可以用脚本做,而且脚本通常更简单、更快。