likes
comments
collection
share

【Redis】【译】Redis 事务(How transactions work in Redis)

作者站长头像
站长
· 阅读数 63

【译】Redis 事务

注:本文是对 Redis 官方文档 《How transactions work in Redis》 的中文翻译,由于中英文表述习惯可能存在差异,会适当给出批注来帮助理解。

本文概要

本文主要内容包括:分析了 Redis 的事务机制,相关命令的使用方法,使用 WATCH 命令实现乐观锁等。

更新记录

  • 2022-08-05:发布

Redis 事务允许一次性执行一组命令,事务操作是围绕着命令 MULTIEXECDISCARDWATCH 展开的。Redis 事务保证如下两点:

  • 事务中的所有命令都是串行化的,并按该串行顺序执行的。在一个事务执行过程中,另一个客户端发出的请求不会在事务执行期间被执行,从而确保确保了事务中的命令是独立执行的。
  • EXEC 命令会触发事务中所有命令开始执行,因此,在客户端调用 EXEC 命令前,若事务上下文与服务器的连接断开了,则事务不执行任何操作,相反则执行所有操作。当使用 AOF 备份时,Redis 会使用一次系统调用 write(2) 将事务写到磁盘上。然而,如果 Redis 服务器发生崩溃,或系统管理员出于某种原因不得不强制关闭服务器,那么就有可能事务操作序列中只有部分操作被执行。Redis 会在重启时检测到这种情况,并返回一个错误。使用 redis-check-aof 工具可以通过删除被追加到 AOF 文件的那部分多余的部分事务操作序列来修复 AOF 文件,从而便于服务器可以再次正常启动。

从 2.2 版开始,Redis 通过以乐观锁的形式对上述两点提供了额外支持,该部分稍后再细说。

使用方法

使用 MULTI 命令可以开启一个 Redis 事务,且总是回复 OK。此时,用户可以发出多个命令。Redis 不会立即执行这些命令,而是将它们排成队列。当执行 EXEC 时,所有的命令都会被一次性执行。反之,调用 DISCARD 则会清空事务队列并退出该事务。例如,原子地递增键 foobar

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

从上例中我们可以清楚地看到,EXEC 返回的是一个结果数组,数组中的每个元素都是事务中单个命令的执行结果,结果顺序与命令发出时顺序相同。当一个 Redis 连接处于一个 MULTI 请求开启的事务上下文时,所有命令都会回复 QUEUED(在 Redis 协议看来 QUEUED 属于状态响应)。当 EXEC 被调用时,事务的操作队列会被简单地调度执行。

事务内部错误

在一个事务中,可能会遇到如下两种命令错误:

  • 命令可能无法入队,因此在调用 EXEC 前可能会出现错误提示。例如,命令可能有语法错误(参数数量、命令名称不对等),或者是由于一些极端条件如内存不足导致的(例如,服务器使用 maxmemory 指令配置了内存上限)。
  • 在调用 EXEC 后命令可能会执行失败,例如,对一个键执行不匹配的操作(对字符串值调用列表操作)。

从 Redis 2.6.5 开始,服务器会在不断将命令入队的过程中检查是否存在错误,如有会在执行 EXEC 期间返回错误,不执行并丢弃该事务。

Redis < 2.6.5 的注意事项: 在 Redis 2.6.5 之前,客户端需要通过检查事务操作队列的返回值来判断在 EXEC 之前是否发生错误:如果回复 queued 说明没问题,否则报错。如果在事务操作入队时出现错误,大多数客户端将中止并丢弃该事务。如果客户端想要继续处理事务,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 返回两个元素(bulk string reply):一个是 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"

使用 CAS 实现乐观锁

(Optimistic locking using check-and-set)

WATCH 命令用于为 Redis 事务提供 CAS(check-and-set)操作,被监视的键的任何改动都会被检测到。在执行 EXEC 前,如果至少有一个被监视的键被修改了,那么整个事务会终止,且 EXEC 返回一个 Null reply 来通知用户事务失败了。

例如,假设我们需要原子地给一个键的值加一(暂时假设 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

使用上面的代码,如果出现竞争条件,在调用 WATCHEXEC 之间的时间内,另一个客户端修改了 val ,那么事务就会失败。

我们只需要重复这个操作,希望这次不会再有新的竞态了。这种形式的锁被称为【乐观锁】。在许多例子中,不同客户端一般会访问不同的键,因此不太可能发生冲突 —— 因此一般情况下不需要重复该操作。

WATCH 命令的解释

那么 WATCH 到底是关于什么的呢?这是一个给 EXEC 加上条件的命令:我们可以要求 Redis 只有在那些监视的键没有被篡改时才执行事务,这里说的篡改包括:客户端执行的修改(如写命令),以及 Redis 自身执行的修改操作,例如过期键删除、缓存淘汰。如果键在 WATCHEXEC 期间被修改,那么整个事务都会终止。

  • 在 Redis 6.0.9 之前,过期键不会导致事务终止,详情见 More on this
  • 事务内的命令不会触发 WATCH 条件,因为它们仅仅是被放入操作队列中,直到调用 EXEC 才会被触发。

WATCH 可以被多次调用。简单地说,所有的 WATCH 调用都具备从调用 WATCHEXEC 之前这段时间的监视效果。你可以使用一个 WATCH 命令监视任意数量的键。当 EXEC 被调用时,所有键都是 UNWATCH 即未监视状态,不管事务是否被中止。当一个客户端连接关闭时,也是如此。

可以使用不带参数的 UNWATCH 命令来清空那些被监视的键。有时,当我们用乐观锁来锁定一些键时,我们可能需要通过执行一个事务来更改这些键,但在读取了键的内容后,我们不想继续处理了。这种情况下我们只需调用 UNWATCH,如此以来当前客户端连接就可以容易地用于开启一个新事务。

使用 WATCH 实现 ZPOP

这里有一个不错的例子,可以说明 WATCH 是如何用于创建(即使是 Redis 不支持的)原子操作的,例如 Redis 的 ZPOP,其中 ZPOPMIN, ZPOPMAX 以及它们的阻塞变量是直到 Redis 5.0 中才引入的,该命令可以从一个有序集合中原子地弹出一个最小值。这里有一个简单实现:

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

如果 EXEC 失败并返回 Null reply, 我们只需要重复该操作就行了。

Redis 脚本和事务

在 Redis 中,关于事务还需考虑的是,redis 脚本也与事务类似,即其具有事务特性。凡是事务可以做的,用脚本也可以,而且还更简洁,执行速度更快。

鸣谢

说明

  • 【Redis】系列相关博客正在更新中,感兴趣的朋友欢迎 star,您的支持是我继续更新下去的最大动力!
  • 由于本人水平、精力有限,文中可能存在疏漏之处,欢迎读者大佬们指正。
  • 对于高质量、格式规范的建议(示例:给出原文具体段落、修改内容、相关依据),确认无误后会合并到博客中,并将贡献者加入【鸣谢】名单中。
  • 可以转载但要注明出处。
转载自:https://juejin.cn/post/7128214664160215071
评论
请登录