交易
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
ed 键被监控以检测它们的变化。如果在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
ed键的情况下才执行事务。这包括客户端所做的修改,如写命令,以及Redis本身所做的修改,如过期或驱逐。如果在WATCH
ed和接收到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 reply),我们只需重复操作。
Redis 脚本和事务
在Redis中,对于类似事务的操作,还需要考虑的是Redis脚本,它们是事务性的。你可以用Redis事务做的任何事情,也可以用脚本来完成,而且通常脚本会更简单、更快。