有道无术,术尚可求,有术无道,止于术。
本系列Redis 版本 7.2.5
源码地址:https://gitee.com/pearl-organization/study-redis-demo
1. 概述
Redis
事务:允许在单个步骤中执行一组命令,其核心命令包括 MULTI
、EXEC
、DISCARD
和 WATCH
。
Redis
事务提供了两个重要的保证:
- 所有事务中的命令都是串行化执行的,按顺序逐一执行。在事务执行过程中,不会在命令执行的中间阶段响应其他客户端的请求,这确保了命令作为单个隔离操作执行。
EXEC
命令触发事务中所有命令的执行:- 如果客户端在调用
EXEC
命令之前在事务上下文中失去了与服务器的连接,那么所有操作都不会执行。 - 一旦调用
EXEC
命令,所有操作将被执行。 - 在使用
AOF
时,确保使用单个系统调用将事务写入磁盘。如果Redi
s服务器崩溃或被系统管理员强制终止,可能会导致仅部分操作被记录。会在重新启动时检测到此条件,并报错退出。通过使用redis-check-aof
工具,可以修复追加写入文件,删除部分事务,以便服务器能够重新启动。
- 如果客户端在调用
自版本 2.2
起,Redis
在上述两个保证之外提供了额外的保证,采用了乐观锁的方式,与检查和设置(CAS
)操作非常类似。
注意:Redis
不支持事务的回滚操作,因为支持回滚会对 Redis
的简单性和性能产生影响。
2. 命令
MULTI
、 EXEC
、 DISCARD
和 WATCH
是 Redis
事务相关的命令。
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
,当被 WATCH
的 key
被修改时,事务才会被执行。如果在事务执行期间这些键被其他客户端修改,那么该事务执行会被打断。
基本语法:
WATCH key [key ...]
示例,客户端监视 k1
:
打开另外一个客户端,对
k1
进行修改操作:
之前的客户端执行事务操作时,
EXEC
返回 nil
:
注意事项:
- 在
Redis 6.0.9
之前的版本中,过期的键不会导致事务中止。 WATCH
命令仅在事务中使用才有意义,因为它是为了配合事务而设计的乐观锁机制。WATCH
可以被调用多次,从调用开始,直到EXEC
调用结束。- 被监视的
key
在事务执行之前,会被Redis
服务器记录下来,并在执行EXEC
命令时检查这些键是否被修改。 - 如果在
WATCH
和EXEC
之间发生了其他客户端的写操作,事务将会失败。此时,客户端可以根据返回值nil
来判断事务执行是否成功。 - 一旦执行
EXEC
或者关闭客户端连接,所有的WATCH
都会被取消。 WATCH
命令在处理并发操作时非常有用,它可以帮助保证事务的原子性和一致性,是实现复杂事务逻辑的重要组成部分。
使用场景:
- 实现乐观锁:
WATCH
命令允许在事务执行前检查键是否被其他客户端修改,从而避免并发冲突,是实现乐观锁的重要手段之一。 - 事务中的条件控制:当事务需要根据某些条件来执行或取消执行时,可以使用
WATCH
来检查条件是否满足。
2.5 UNWATCH
UNWATCH
命令用于取消 WATCH
命令对所有 key
的监视。如果执行过 EXEC
或 DISCARD
,会自动取消监视,无需再执行 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
才会同时存在两种处理事务的方法。
不过官方并不打算在短时间内就移除事务功能, 因为事务提供了一种即使不使用脚本, 也可以避免竞争条件的方法, 而且事务本身的实现并不复杂。
不过在不远的将来, 可能所有用户都会只使用脚本来实现事务也说不定。 如果真的发生这种情况的话, 那么将废弃并最终移除事务功能。