【Redis】【译】Redis 事务(How transactions work in Redis)
【译】Redis 事务
- 原文标题:How transactions work in Redis
- 原文地址:redis.io/docs/manual…
- 文章状态:更新中
注:本文是对 Redis 官方文档 《How transactions work in Redis》 的中文翻译,由于中英文表述习惯可能存在差异,会适当给出批注来帮助理解。
本文概要
本文主要内容包括:分析了 Redis 的事务机制,相关命令的使用方法,使用 WATCH
命令实现乐观锁等。
更新记录
- 2022-08-05:发布
序
Redis 事务允许一次性执行一组命令,事务操作是围绕着命令 MULTI
、EXEC
、DISCARD
和 WATCH
展开的。Redis 事务保证如下两点:
- 事务中的所有命令都是串行化的,并按该串行顺序执行的。在一个事务执行过程中,另一个客户端发出的请求不会在事务执行期间被执行,从而确保确保了事务中的命令是独立执行的。
EXEC
命令会触发事务中所有命令开始执行,因此,在客户端调用EXEC
命令前,若事务上下文与服务器的连接断开了,则事务不执行任何操作,相反则执行所有操作。当使用 AOF 备份时,Redis 会使用一次系统调用write(2)
将事务写到磁盘上。然而,如果 Redis 服务器发生崩溃,或系统管理员出于某种原因不得不强制关闭服务器,那么就有可能事务操作序列中只有部分操作被执行。Redis 会在重启时检测到这种情况,并返回一个错误。使用redis-check-aof
工具可以通过删除被追加到 AOF 文件的那部分多余的部分事务操作序列来修复 AOF 文件,从而便于服务器可以再次正常启动。
从 2.2 版开始,Redis 通过以乐观锁的形式对上述两点提供了额外支持,该部分稍后再细说。
使用方法
使用 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 协议看来 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
使用上面的代码,如果出现竞争条件,在调用 WATCH
和 EXEC
之间的时间内,另一个客户端修改了 val
,那么事务就会失败。
我们只需要重复这个操作,希望这次不会再有新的竞态了。这种形式的锁被称为【乐观锁】。在许多例子中,不同客户端一般会访问不同的键,因此不太可能发生冲突 —— 因此一般情况下不需要重复该操作。
WATCH 命令的解释
那么 WATCH
到底是关于什么的呢?这是一个给 EXEC
加上条件的命令:我们可以要求 Redis 只有在那些监视的键没有被篡改时才执行事务,这里说的篡改包括:客户端执行的修改(如写命令),以及 Redis 自身执行的修改操作,例如过期键删除、缓存淘汰。如果键在 WATCH
和 EXEC
期间被修改,那么整个事务都会终止。
注
- 在 Redis 6.0.9 之前,过期键不会导致事务终止,详情见 More on this。
- 事务内的命令不会触发
WATCH
条件,因为它们仅仅是被放入操作队列中,直到调用EXEC
才会被触发。
WATCH
可以被多次调用。简单地说,所有的 WATCH
调用都具备从调用 WATCH
到 EXEC
之前这段时间的监视效果。你可以使用一个 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