Redis Transactions

内容纲要

在Redis中事务如何工作

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之前,客户端需要通过检查queued命令的返回值来检测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
-ERR Operation against a key holding the wrong kind of value

EXEC返回两个元素的批量字符串应答,其中一个是OK代码,另一个是-ERR应答。客户端库需要找到一种合理的方式将错误提供给用户。

需要注意的是,即使一个命令失败,队列中的所有其他命令也会被处理——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)行为。

监视键是为了检测它们的变化。如果在执行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的结果,那么事务将失败。

我们只需要重复这个操作,希望这次不会出现新的比赛。这种形式的锁定称为乐观锁定。在许多用例中,多个客户端将访问不同的键,因此不太可能发生冲突——通常不需要重复操作。

WATCH 解释

那么WATCH到底是关于什么的呢?这是一个使EXEC有条件的命令:我们要求Redis只在所有被监视的键都没有被修改的情况下才执行事务。这包括客户端所做的修改,比如写命令,以及Redis本身所做的修改,比如过期或删除。如果在监视密钥和接收EXEC之间修改了密钥,则整个事务将被中止。

请注意

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

WATCH可以被多次调用。简单地说,所有的WATCH调用都具有从调用开始观察变化的效果,直到调用EXEC。您还可以向单个WATCH调用发送任意数量的键。

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

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

使用WATCH实现ZPOP

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

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

如果EXEC失败(即返回Null应答),我们只是重复操作。

Redis脚本和事务

还有一些事情需要考虑,比如redis中的事务操作是事务性的redis脚本。你可以用Redis事务做的所有事情,你也可以用脚本做,通常脚本会更简单和更快。

Leave a Comment

您的电子邮箱地址不会被公开。 必填项已用*标注

close
arrow_upward