民工兄弟Redis教程(六):事务详解
什么是Redis事务
Redis事务的核心是一组命令。事务支持同时执行多个命令,并且事务中的所有命令都会被序列化。交易执行过程中,排队的命令会按顺序执行,其他客户端发送的命令请求不会插入到交易执行命令序列中。
综上所述:redis 事务是对队列中一系列命令的一次性、顺序且独占的执行。
Redi 的事务相关命令及使用方法
MULTI、EXEC、DISCARD、WATCH 是Redi 的事务相关命令。
- MULTI:一旦事务开始,redis会将连续的命令逐一放入队列中,然后使用EXEC命令原子地执行这一系列命令。
- EXEC:执行事务中的所有操作命令。
- DISCARD:中止事务并中止事务块中所有命令的执行。
- SE:监控一个或多个按键。如果在提交事务之前该键(或多个键)被其他命令更改,则事务将被中止,并且事务中的任何命令都不会被执行。
- UNWATCH:停止WATCH对所有按键的监控。
标准交易执行
分别为k1和k2赋值,在交易中修改k1和k2,执行交易后检查k1和k2的值是否发生变化。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"22"
127.0.0.1:6379>
更多Redis学习文章请参见:NoSQL数据库系列-Redis。该系列持续更新。
交易取消
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 33
QUEUED
127.0.0.1:6379> set k2 34
QUEUED
127.0.0.1:6379> DISCARD
OK
交易错误处理
- 语法错误(编译器错误)
开启交易后,将k1值更改为11,k2值更改为22,但事件导致k2语法错误交易提交失败,k1和k2保留原来的值。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> sets k2 22
(error) ERR unknown command `sets`, with args beginning with: `k2`, `22`,
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>
- Redis类型错误(运行时错误)
启动事务后,将k1值更改为11,k2值更改为22,但使用k2的类型为List。在运行时检测到拼写错误,最终导致提交交易时出现错误。 ,此时事务不会回滚而是跳过错误命令继续执行。结果,k1 的值发生变化,k2 保持其原始值。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k1 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> lpush k2 22
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>
CAS 操作实现乐观锁定
WATCH 命令可以为 Redis 事务提供检查和设置 (CAS) 行为。
- CAS?乐观锁?官方的 Redis 示例可以帮助您了解 WATCH 的
键将受到监视,并找出这些键是否发生更改。如果在执行 EXEC 之前至少更改了一个受监视的键,则整个事务将被取消,并且 EXEC 将返回空响应以指示事务已失败。
例如,假设我们需要将一个值自动加 1(假设 INCR 不存在)。
首先我们可以这样做:
val = GET mykey
val = val + 1
SET mykey $val
当只有一个客户端时,上述实现可以很好地工作。然而,当多个客户端同时对同一个密钥执行此类操作时,就会出现竞争条件。例如,如果客户端 A 和 B 都读取了 key 的原始值,比如 10,则两个客户端都会将 key 的值设置为 11,但正确的结果应该是 12。
使用 WATCH 我们可以轻松解决这个问题类问题:
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
使用上面的代码时,如果其他客户端在执行WATCH之后、执行EXEC之前更改了mykey的值,那么当前客户端的Transaction将会失败。程序必须做的是不断重试此操作,直到不发生冲突为止。
这种形式的锁定称为乐观锁定,它是一种非常强大的锁定机制。而且由于在大多数情况下,不同的客户端将访问不同的密钥,因此冲突通常很少见,因此通常不需要重试。
- 实现如何监控时钟?
Redis 使用 WATCH 命令来决定是继续执行还是回滚事务。接下来需要在MULTI之前使用WATCH监控某些键值对,然后使用MULTI命令开启交易,执行数据结构操作的各种命令。 ,此时这些命令正在排队。
当你使用 EXEC 提交事务时,它会首先比较 WATCH 监视的键值对。如果没有变化,则执行交易队列中的命令并发送交易;如果有变化,则不会提交事务。事务回滚时发出任何命令。当然,无论是否回滚,Redis在提交事务之前都会取消WATCH命令。
- watch命令实现监控
在事务开始前使用WATCH监控k1,然后将k1更改为11,表明在事务开始前k1值被更改。MULTI启动事务,将k1值更改为12,k2更改为22,并执行 EXEC ,返回零,表示事务正在回滚;检查k1和k2的值没有被事务中的命令改变。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> set k1 11
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 12
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>
- UNWATCH取消监视
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> set k1 11
OK
127.0.0.1:6379> UNWATCH
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 12
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"12"
127.0.0.1:6379> get k2
"22"
127.0.0.1:6379>
更多Redis学习文章请参见:NoSQL数据库系列中的Redis。该系列持续更新。
Redi 的事务执行阶段
通过上面的命令执行,很明显,Redi 的事务执行分为三个阶段:
- Open:用 MULTI 启动一个事务
- 将这些事务命令入队:命令不会被执行收到后立即执行,但会被放入事务队列等待执行
- 执行:事务由 EXEC 命令触发
当客户端切换到事务状态时,客户端发送的服务器命令将执行不同的操作:
- 如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令之一,则服务器立即执行该命令。
- 相反,如果客户端发送的命令是EXEC、DISCARD、WATCH、MULTI这四个命令之外的命令,那么服务器端不会立即执行该命令,而是将该命令放入事务队列中。然后向客户端返回队列响应。

更深入的了解
让我们再过几个问题来更深入地了解Redis事务。
为什么Redis不支持重置?
如果您有使用关系型数据库的经验,那么“Redis 在事务失败时不回滚,而是继续执行剩余命令”的做法对您来说可能会显得有些陌生。
以下是这种方法的优点:
- Redis 命令只会因语法不正确而失败(并且这些问题在排队期间无法发现),或者命令用在错误类型的 key 上:这也是如此从实际角度来看,失败的命令是由编程错误引起的,这些错误应该在开发过程中检测到,不应该出现在生产环境中。
- 因为不需要回滚支持,所以Redis的内部可以保持简单和快速。
人们相信 Redis 处理事务的方式会导致错误。但需要注意的是,一般情况下,回滚并不能解决编程错误带来的问题。例如,如果你本来想通过 INCR 命令将某个 key 的值加 1,但不小心加了 2,或者对错误类型的 key 执行了 INCR,回滚就没有办法处理这些情况。
如何理解Redis和事务的ACID?
一般来说,事务有四个属性,称为ACID,分别是原子性、一致性、隔离性和持久性。这是基础,但是很多文章对Redis是否支持ACID存在一些异议。我觉得有必要明确一下:
- 原子性
首先通过上面我们知道,运行时错误是不会被回滚的。许多文章因此指出Redis事务打破了原子性;而官方文档认为它们符合原子性。
Redis官方文档给出的理解是Redis事务是原子的:所有命令要么全部执行,要么根本不执行。而且并不完全成功。
- 一致性
Redis事务可以保证命令失败时可以回滚,数据可以恢复到执行前的样子,保证一致性,除非redis进程意外终止。
- 隔离性
Redis 事务严格遵守隔离性。原因是redis是单线程、单进程模式(v6.0之前),可以保证命令执行过程不被其他客户端命令打断。
但是,Redis 没有像其他结构化数据库那样的隔离级别设计。
- 持久性
Redis 事务不保证持久性。这是因为redis持久化策略中的RDB和AOF都是异步执行的。出于性能原因,不保证耐用性。
Redis事务的其他实现
- 基于Lua脚本,Redis可以保证脚本中的命令一次性执行并按顺序执行。它也不提供事务执行错误的回滚。如果某些命令在执行过程中失败,剩余的命令将继续运行直到完成
- 基于中间标志变量,另一个标志变量用于标识事务是否已完成。读取数据时,首先读取字段变量来判断事务是否完成。但是这样就需要额外的代码来实现,比较麻烦
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。