Redis 事务管理

Redis 提供了五个指令用于处理事务:MULTIEXECDISCARDWATCHUNWATCH,这五个命令是 Redis 进行事务处理的基础。

这些指令允许一组命令在一个步骤中执行,其中有两个重要的保证:

  • 一个事务中的所有命令都被序列化并按顺序执行。在 Redis 事务的执行过程中该事务服务于另一个客户端发出的请求的情况永远不会发生。这保证了被执行的命令将会被视为一个单独的隔离操作

  • 要么处理所有命令,要么不处理任何命令,因此 Redis 事务具备原子性。EXEC 命令触发所有的命令在一个事务中执行,因此,如果客户端在调用 EXEC 命令之前在事务上下文中失去与服务器的连接,则不会执行任何操作,相反,如果调用 EXEC 命令,则执行所有操作。当使用 AOF 的数据持久化方式时,Redis 将确保使用一个单独的 write 系统调用来将事务写入到磁盘中。然而,如果 Redis 服务崩溃了,或者被系统管理员以一种强硬的方式杀死,这种情况下可能会导致只保存了部分数据。Redis 在重启时将检测这个情况,如果出现了错误将会退出。通过 redis-check-aof 工具可以修复这个问题,通过移除这一部分的事务数据使得 Redis 服务能够重新启动

Redis 2.2 版本开始,Redis 允许对上述两个保证提供额外的保证,通过乐观锁(与 CAS 的方式类似)来实现。

具体使用

  • MULTIEXECDISCARD

    MULTIRedis 事务的进入点,这个命令执行之后总是返回 “OK”。执行此命令之后,用户可以输入多条命令,而不是执行这行命令,Redis 会将这些命令进行排队。所有输入的命令只有在输入 EXEC 命令时才会被实际执行;输入 DISCARD 命令将会当前事务的命令队列,并且退出当前事务

    20220225 194616 的屏幕截图png

    可以看到,Redis 执行 EXEC 命令之后,会依次按照执行顺序输出执行结果

  • WATCHUNWATCH

    WATCH 用于监视 Key 是否发生改变,如果在事务执行过程中被监测的 Key 被修改了,那么整个事务都将被丢弃。

    使用 UNWATCH 可以在事务执行过程中取消对 Key 的监视

事务异常的处理

在一个事务的执行过程中可能会遇到两种类型的命令错误:

  • 一个命令在i进入执行队列的过程中入队失败,因此在调用 EXEC 命令执行这个事务之前可能会出现错误。例如,这个命令在语法上就是错误的(错误的参数个数、错误的命令名),或者一些危险的情况,如:内存溢出(如果已经将 Redis 服务通过 maxmemory指令设置了最大内存大小)
  • 一个命令在调用 EXEC 执行事务之后失败,例如,对具有错误值的键执行了操作(就像针对字符串值调用 list 操作)

客户通常会遇到第一种错误(发生在 EXEC 调用之前),对于这种情况,通过检查排队命令的返回值来进行不同的处理:如果命令以 QUEUED 回复,则表示该命令正确入队,否则 Redis 返回错误。 如果在对命令进行排队时出现错误,大多数客户端将中止当前事务并丢弃它。

Redis 2.6.5 开始,Redis Server 会记住在命令排队过程中发生的错误,如果出现错误则拒绝执行事务。在执行过程中也会返回错误,并且会自动丢弃事务。

如果遇到的是第二种类型的错误(发生在 EXEC 调用之后),Redis 对于这种错误不会以特殊的方式进行处理:即使在事务期间某些命令失败,所有其他命令也将被执行。

事务的回滚

注意: Redis 不支持事务的回滚操作,这点和传统的关系型数据库不同

对于此,Redis 官方有以下几点解释:

  • Redis 命令只有在使用错误的语法调用时才会失败(并且在命令入队期间无法检测到问题),或者对支持命令操作的数据类型执行命令:这意味着实际上失败的命令是编程错误的结果,并且是一种很可能在开发过程中检测到的错误,而不是在生产中。
  • 由于不需要回滚的功能,因此 Redis 的内部会更加简单和快速。

反对以上两个观点的一个论点是 bug 的产生,但是应该注意的是,通常回滚不会使您免于编程错误。例如,如果一个命令将键增加 2 而不是 1,或者增加操作到了错误的键,则回滚机制无法提供帮助。 鉴于没有人可以将程序员从他或她的错误中拯救出来,并且 Redis 命令失败所需的错误不太可能进入生产环境,我们选择了不支持错误回滚的更简单和更快的方法。

乐观锁

WATCH 命令被用于提供给事务 CAS 的行为

WATCH 命令指定的 Key 将会被监视,以检测其它事务对它的修改。如果至少有一个被 WATCHKey 在调用 EXEC 命令之前被修改,那么当前正在执行的整个事务都将被丢弃,并且 EXEC 命令将会返回一个 Null Reply 来提示当前的事务是执行失败的

例如,想象一下我们需要原子性地将一个 Key 的值增加 \(1\) ,按照一般的编程思想应该是首先获取 Key 的值,然后将 Key 的值加 \(1\),然后再将增加后的值放回到原来的 Key 中,伪代码如下所示:

val = GET k1
val = val + 1
SET k1 $val

这种方式只有在给定的时间段、只有一个单独的线程执行此项操作时才是可靠的,如果有多个客户端尝试通过类似的方式在相同的时间段内进行增值操作,那么这将导致 “竞态条件” 的产生。例如,客户端 A 和客户端 B 都将读取 k1 的旧值,假设旧值为 \(10\),如果这两个客户端在同一时刻读取到了旧值,并且增加了 \(1\),那么最后这两个客户端都进行 SET 操作时,最终的结果将会是 \(11\) 而不是 \(12\)

得益于 WATCH 命令的存在,使得上面的问题能够得到很好的解决:

WATCH k1
val = GET k1
val = val + 1
MULTI
SET k1 $val
EXEC

通过上面的方式,如果存在竞态条件,并且在我们执行 WATCH 命令到 EXEC 命令的执行区间中,有其它的客户端对 k1 的值进行了修改,那么我们当前的这个事务将会失败

我们只需要重新执行一次上面的代码逻辑即可(希望这次不会遇到新的竞态)。这种锁定形式被称为 “乐观锁”,是一种非常强大的锁的表现形式。在大多数情况下,多个客户端将会访问不同的 Key,因此不太可能发生碰撞(通常不需要重复操作)。

WATCH 命令的解释

WATCH 到底是什么?这是一个使得 EXEC 命令具有条件的命令:我们要求 Redis 仅在未修改任何被 WATCH 命令监视的键的情况下执行事务。这包括客户端所做的修改,如:写入命令;以及 Redis 本身所做的修改,如过期或清除。如果在事务遇到 EXEC 命令之前,被监视的 Key 被修改了,那么整个事务都将被丢弃

注意: 在 6.0.9 之前的 Redis 版本中,过期的 Key 不会导致事务中止。

WATCH 命令可以被多次调用。 简单地说,所有的 WATCH 调用都将具有监视从 WATCH 调用开始到调用 EXEC 之前的更改的效果。您还可以将任意数量的键发送到单个 WATCH 调用

当调用 EXEC 时,所有的 Key 都是 UNWATCHed(取消监视) 状态,不管事务是否被中止。 此外,当客户端连接关闭时,一切都会被取消监视。

也可以使用 UNWATCH 命令(不带参数)来取消所有被监视的键的监视。有时这很有用,因为我们乐观地锁定了一些键,因为可能我们需要执行事务来读取这些键,但是在读取键的当前内容后,我们不想继续检测这个键。

Redis 脚本和事务

Redis 脚本在定义上是具有事务性的,所以你可以用 Redis 事务做的所有事情,你也可以用脚本做,通常脚本会更简单更快。

这种重复是由于脚本是在 Redis 2.6 中引入的,而事务早已存在。 但是,我们不太可能在短期内取消对事务的支持,因为从语义上讲,即使不使用 Redis 脚本,仍然可以避免竞争条件,特别是因为 Redis 事务的实现复杂性很小。

然而,在不久的将来,我们会看到整个用户群都只是在使用脚本,这并非不可能。 如果发生这种情况,我们可能会弃用并最终事务。


参考:

[1] https://mp.weixin.qq.com/s/bs5NfSkQlFbFp7KMQ9aLmw

[2] https://redis.io/topics/transactions

posted @ 2022-03-17 16:38  FatalFlower  阅读(125)  评论(0编辑  收藏  举报