Redis 7.x 系列【18】事务

25 阅读8分钟

有道无术,术尚可求,有术无道,止于术。

本系列Redis 版本 7.2.5

源码地址:https://gitee.com/pearl-organization/study-redis-demo

1. 概述

官方文档

Redis 事务:允许在单个步骤中执行一组命令,其核心命令包括 MULTIEXECDISCARDWATCH

Redis 事务提供了两个重要的保证:

  • 所有事务中的命令都是串行化执行的,按顺序逐一执行。在事务执行过程中,不会在命令执行的中间阶段响应其他客户端的请求,这确保了命令作为单个隔离操作执行。
  • EXEC 命令触发事务中所有命令的执行:
    • 如果客户端在调用 EXEC 命令之前在事务上下文中失去了与服务器的连接,那么所有操作都不会执行。
    • 一旦调用 EXEC 命令,所有操作将被执行。
    • 在使用AOF时,确保使用单个系统调用将事务写入磁盘。如果Redis服务器崩溃或被系统管理员强制终止,可能会导致仅部分操作被记录。会在重新启动时检测到此条件,并报错退出。通过使用 redis-check-aof 工具,可以修复追加写入文件,删除部分事务,以便服务器能够重新启动。

自版本 2.2 起,Redis在上述两个保证之外提供了额外的保证,采用了乐观锁的方式,与检查和设置(CAS)操作非常类似。

注意Redis 不支持事务的回滚操作,因为支持回滚会对 Redis 的简单性和性能产生影响。

2. 命令

MULTIEXECDISCARDWATCHRedis 事务相关的命令。

2.1 MULTI

MULTI 命令用于开启一个事务,它总是返回 OK MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC命令被调用时, 所有队列中的命令才会被执行。

当客户端处于事务状态时, 所有传入的命令都会返回一个内容为 QUEUED 的状态回复, 这些被入队的命令将在 EXEC 命令被调用时执行。

示例:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1 
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED

2.2 EXEC

EXEC 命令用于执行事务队列内的所有命令,返回值为事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil

示例:

127.0.0.1:6379(TX)> exec 
1) OK
2) OK

2.3 DISCARD

DISCARD 命令用于取消事务,放弃执行事务队列内的所有命令,恢复连接为非事务模式。

示例:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> discard 
OK

2.4 WATCH

WATCH 命令用于监视一个或多个 key ,当被 WATCHkey 被修改时,事务才会被执行。如果在事务执行期间这些键被其他客户端修改,那么该事务执行会被打断。

基本语法:

WATCH key [key ...]

示例,客户端监视 k1在这里插入图片描述 打开另外一个客户端,对 k1 进行修改操作: 在这里插入图片描述 之前的客户端执行事务操作时,EXEC 返回 nil在这里插入图片描述

注意事项:

  • Redis 6.0.9之前的版本中,过期的键不会导致事务中止。
  • WATCH 命令仅在事务中使用才有意义,因为它是为了配合事务而设计的乐观锁机制。
  • WATCH 可以被调用多次,从调用开始,直到EXEC调用结束。
  • 被监视的 key 在事务执行之前,会被Redis服务器记录下来,并在执行 EXEC 命令时检查这些键是否被修改。
  • 如果在 WATCHEXEC 之间发生了其他客户端的写操作,事务将会失败。此时,客户端可以根据返回值 nil 来判断事务执行是否成功。
  • 一旦执行 EXEC 或者关闭客户端连接,所有的 WATCH 都会被取消。
  • WATCH 命令在处理并发操作时非常有用,它可以帮助保证事务的原子性和一致性,是实现复杂事务逻辑的重要组成部分。

使用场景:

  • 实现乐观锁:WATCH 命令允许在事务执行前检查键是否被其他客户端修改,从而避免并发冲突,是实现乐观锁的重要手段之一。
  • 事务中的条件控制:当事务需要根据某些条件来执行或取消执行时,可以使用 WATCH 来检查条件是否满足。

2.5 UNWATCH

UNWATCH 命令用于取消 WATCH 命令对所有 key 的监视。如果执行过 EXECDISCARD,会自动取消监视,无需再执行 UNWATCH

示例:

localhost:0>UNWATCH
"OK"

3. 事务中的错误

Redis 事务中,可能会遇到两种类型的命令错误:

  • 在调用EXEC之前,某些命令可能会失败无法入队。例如,命令可能存在语法错误,或者可能出现关键条件,例如内存不足等。
  • 在调用EXEC之后,某些命令可能会失败。例如,可能对一个键执行了与其值类型不符的操作,例如对字符串值执行列表操作等。

Redis 2.6.5 开始,服务器会在累积命令期间检测到错误。如果发现错误,它将拒绝执行事务并在EXEC期间返回错误,丢弃该事务。而在EXEC之后发生的错误不会以特殊方式处理,即使事务中的某些命令失败,其他所有命令仍将被执行。

示例,在调用EXEC之前的命令存在语法错误时,不会被添加到队列:

localhost:0>MULTI
"OK"
localhost:0>INCR a b c
"ERR wrong number of arguments for 'incr' command"

示例,命令的语法都是正确的,在调用EXEC之后,由于 LPOP 操作的对象是一个字符串,所以这个命令执行失败,但是队列中的其他命令仍会被执行:

localhost:0>MULTI
"OK"
localhost:0>SET a abc
"QUEUED"
localhost:0>LPOP a
"QUEUED"
localhost:0>SET b efg
"QUEUED"
localhost:0>EXEC
 1)  "OK"
 2)  "WRONGTYPE Operation against a key holding the wrong kind of value"
 3)  "OK"

4. 和数据库事务的区别

关系型数据库事务:事务保证一系列操作要么全部成功,要么全部失败,如果某一步出现了异常,数据就会回滚,把之前的操作撤销。数据库事务需要满足四大特性(简称ACID),即原子性隔离性持久性一致性,才能保证数据正确性。

4.1 原子性

关系型数据库事务具有严格的原子性,要么所有操作都执行成功并永久保存(提交),要么所有操作都不执行(回滚)。

Redis 事务允许一组命令作为一个原子操作进行执行。但是,不是严格的原子性,如果在执行期间发生错误,部分命令可能已经执行,而另一部分可能未执行。

4.2 隔离性

关系型数据库事务通过锁和多版本并发控制(MVCC)等机制来实现不同事务之间的隔离性,例如读未提交、读已提交、可重复读和串行化等级别。

Redis事务提供了一定的隔离性,但没有像传统数据库那样严格的隔离级别。不同客户端的事务可能会相互干扰,需要通过WATCH命令显式地实现乐观锁机制。

4.3 持久性

关系型数据库事务中,事务提交后,数据会被持久化到磁盘,保证数据不会丢失。

Redis 通过持久化机制来实现持久性,但在事务提交时并没有严格保证数据已经永久保存。

4.4 一致性

关系型数据库事务中,发生错误时,数据库可以进行回滚操作,保证事务执行前后数据的一致性。

Redis事务中的某个命令执行失败,后续的命令仍然会继续执行,不会回滚已经执行的命令,无法保证一致性。

5. 脚本和事务

从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。

因为脚本功能是 Redis 2.6 才引入的, 而事务功能则更早之前就存在了, 所以 Redis 才会同时存在两种处理事务的方法。

不过官方并不打算在短时间内就移除事务功能, 因为事务提供了一种即使不使用脚本, 也可以避免竞争条件的方法, 而且事务本身的实现并不复杂。

不过在不远的将来, 可能所有用户都会只使用脚本来实现事务也说不定。 如果真的发生这种情况的话, 那么将废弃并最终移除事务功能。