Redis 4.x系列(九):Redis Transaction(事务)
Redis 中的事务与传统数据库的事务存在较大的差异,所以理解 Redis 事务,必须跳出传统数据库事务的概念,这是由两者对事务的实现方式不同决定的。
Redis 官方文档对 Redis 事务在处理所有命令的描述是:要么处理所有命令,要么都不处理,因此 Redis 事务也是原子的。 Redis Transactions 官方文档。注:Redis 事务确保原子性的时机不同于传统数据库的事务处理,详见下文。
事务对比
以 MySQL为例,对 Redis 事务 与 传统数据库的事务特性做个简单的比较。
事务执行流程
Redis 事务的执行流程不同于传统数据库的事务处理流程。
传统数据库事务处理:
- BEGIN:开启一个事务
- COMMIT:提交事务,修改持久化
- ROLLBACK:事务l回滚,并撤销所有正在进行但未提交的修改
MySQL 事务是基于
UNDO/REDO
日志实现的UNDO
日志记录修改前状态,ROLLBACK基于UNDO日志实现;REDO
日志记录修改后的状态,COMMIT
基于REDO日志实现。
MySQL 开启事务,在 COMMIT 之前的
insert,update,delete
操作会被立即执行并返回结果,但没有持久化写入硬盘,这里涉及到事务隔离级别另说,如果其中某一步骤出问题,则所有操作都会回滚。 执行 COMMIT操作,修改才被写入到硬盘。Redis 事务处理:
- MULTI:标记事务开始
- EXEC:按顺序执行 commands 队列中的命令
- DISCARD:结束事务,不执行任何命令,并清除队列,连接状态恢复正常。
Redis 事务是基于
commands
队列实现的,默认未开启事务,命令会被立即执行并返回结果;开启事务,命令并不会立即执行,而是进入队列,只有调用了EXEC
才会按顺序执行队列中的命令。Redis 事务不支持回滚,官方文档里给出的解释是:只有在语法错误时,命令才会执行失败,而语法错误的命令在排队期间无法被检测到;或数据类型错误导致执行失败,意味着失败的命令是编程错误的结果,应在开发过程中就检测出错误,而不会在生产中。Redis 命令失败的错误类型不太可能进入生产中,所以选择了更简单、更快速的方法,即不支持错误回滚。
从上可以看出,两者在事务处理上存在明显的区别:
MySQL 事务中涉及事务操作会被立即执行并返回结果,若执行出现错误,则需要ROLLBACK回滚,SQL语法错误或内部错误(如主键重复)只能在执行时才能被检测到并报错;
Redis 事务中的命令不会被立即执行,而是进入队列,调用了 EXEC
才会被执行,在命令进入队列的操作时就对命令的语法进行了检测,若存在语法错误则直接清除commands对列,并闭事务,可理解为在事务中能被执行的命令都是正确的。
相对于MySQL,可理解为 Redis 事务对命令的语法检测提前到在将命令加入队列时进行,而不是执行命令时,屏蔽了存在语法错误的 commands 队列被执行。当然还可能出现在执行时才能检测到的类型错误,这又回到了 Redis 的官方解释。
事务四大特性
- 原子性:所有操作要么全部成功,要么全部失败回滚。
- MySQL:开启了事务,事务中的操作会被立即执行并返回结果,在事务中的执行出现异常则撤消未提交的修改(回滚)。
- Redis:开启了事务,操作命令被加入到队列不会被立即执行,在加入队列过程中检测语法,若出现错误则清空队列并关闭事务;
EXEC
命令触发事务中所有命令的执行,若执行出现错误,所有其它命令仍将被执行并有效,Redis 不支持回滚。以传统关系数据库对事务原子性的定义,则 Redis是非原子的;若以对语法错误的命令处理,可认为 Redis 是原子性的。
- 一致性:事务必须使数据库从一个一致性状态变换到另一个一致性状态(事务执行前后都必须处于一致性状态)。
- MySQL:MySQL的事务处理机制是确保状态一致性的(强一致)。
- Redis:因 Redis 不支持回滚,也就放弃了对数据一致性的保证。不过对于一个高性能的内存数据库,数据操作一致性更多应该依赖于应用层面,在分布式和集群环境,数据一致性无法依靠事务机制来保证,更多是在应用层处理来是追求最终一致性。
- 隔离性:并发事务之间相互隔离不被干扰。
- MySQL:存在并发事务,事务之间存在被干扰的可能,所以 MySQL 定义了 5 个事务隔离级别,根据需要可对事务进行分级控制。
- Redis:单线程模型,也就不存在并发事务,在一个事务完成之前其它客户端提交的各种操作都不能执行,也就保证了隔离性。
- 持久性:事务被提交,数据的改变就是永久性的。
- MySQL:在事务提交后,数据会被写入到硬盘,数据的改变是永久性的。
- Redis:内存数据库,持久性是无法保证的。但 Redis 也提供了 2 种数据持久化模式
AOP
和RDB
(可看作某一个时间点的快照)
Redis 事务机制
MULTI, EXEC, DISCARD, WATCH
是 Redis 事务的基础。Redis 事务允许一个步骤中执行一组命令,并且带有以下两个保证:
- Redis 事务中的所有命令都被序列化并按顺序的执行。在执行 Redis 事务的过程中,不会处理另外一个客户端的请求,这可以确保这些命令作为一个单独隔离操作来执行。
- 所有命令要么都处理,要么都不处理,因此 Redis 事务也是原子的。
EXEC
命令触发事务中所有命令的执行,因此如果客户端在调用标记开始事务的MULTI
命令之前丢失了与服务器的连接,则不执行任何操作;相反,如果调用到了EXEC
命令,则执行所有操作。
从版本2.2开始,Redis 允许对上述两条提供额外的保证,采用乐观锁的实现方式。
Redis 事务用法
- 使用
MULTI
标记开始一个事务,该命令的返回的始终是 OK。 - 此时,用户可以发出多个命令。Redis 不会执行这些命令,而是将它们排队。当开启了 Redis 事务,所有命令都将使用字符串
QUEUED
回复。排队命令在调用EXEC时被调度执行。 - 调用
EXEC
,所有命令都会被执行,返回一个数组,其中每个元素都是事务中单个命令的回复,其顺序与发出命令的顺序相同。 - 或者调用
DISCARD
命令清空事务队列并退出事务。
示例:
1 | 127.0.0.1:6379> multi |
Redis 事务错误处理
在事务期间,可能会遇到两种命令错误:
- 命令可能无法排队,在调用EXEC之前可能出错,队列会被清空并退出事务。例如,命令可能语法错误(错误的参数数量,错误的命令名称等),或者一些关键条件不满足,如内存不足(如果配置了 maxmemory 限制)。
- 调用
EXEC
后,队列中的的某一个命令执行可能失败,但 Redis 不会停止执行命令, 队列中的所有其它正确的命令仍会被执行成功。 例如,对错误值的键进行操作(比如对字符串型的值执行INCR
操作),这类错误通常是类型错误。
客户端通过检查排队命令的返回值来感知在EXEC调用之前发生的第一类错误:如果命令回复QUEUED,则它正确排队,否则 Redis 会返回错误,如果在排队时命令出错,大多数客户端中止该事务。若是在调用EXEC
之后发生的错误,即使某些命令在事务期间失败,所有其它命令也将被执行。
还有一种更复杂的错误场景,在管道中使用了事务,从 Redis 2.6.5 开始,服务器会记住命令在打包期间的错误,并且拒绝执行事务(事务期间的命令不被执行),在EXEC
期间返回错误,并自动丢弃事务。
示例:
1 | -- 命令语法错误,无法排队 |
Redis WATCH-乐观锁
- WATCH:Redis 的
WATCH
命令为 Redis 事务提供了CAS
(check-and-set)特性;WATCH
命令是基于乐观锁的实现。
在事务开启前使用WATCH
命令对目标键加乐观锁(监视),如果监视的任意一个 Key的值在调用EXEC
之前被修改,则整个事务中止,并且EXEC
返回**(nil)**通知事务失败。 - UNWATCH:对已加锁的目标键解锁。可以使用UNWATCH命令,不带任何参数来清除所有键的乐观锁(WATCH)。
WATCH使EXEC
成为条件命令,只有监视的键没有任何修改,才会执行事务。单个WATCH可以对多个键加乐观锁。WATCH的生命周期从事务开始是开始到调用EXEC时结束。当调用EXEC时,无论事务是否中止,所有键都是UNWATCH
,当客户端连接关闭时,所有都是UNWATCH。也可以在事务中使用UNWATCH解锁,让后续操作不被WATCH,可使连接用于新事务(这句转译的有些别扭,使用方式感觉没啥用)。
示例:
1 | 127.0.0.1:6379> watch age |
使用WATCH创建新的原子操作
示例:从有序集合中自动弹出分数较低的元素(实际开发可通过代码或脚本实现)
1 | 127.0.0.1:6379> zadd zset 0 apple |
Redis 脚本和事务
根据定义,Redis 脚本是事务性的,所以在 Redis 事务中可以执行的所有操作也可使用脚本操作,通常脚本将更简单、更快速。
这种重复是因为在Redis 2.6中才引入了脚本,而事务早就存在,不可能在短时间内删除对事务的支持,因为即使不使用 Redis 脚本,它仍然可以避免竞争条件(Race Conditions),同时因为 Redis 事务的实现复杂性很小。
若未来出现用户只使用脚本的情况,Redis 事务可能会被弃用并最终删除。
Redis事务和乐观锁
基于 Spring Boot 提供的自动配置来执行事务,在注册 RedisTemplate Bean 时开启事务支持:template.setEnableTransactionSupport(true);或获取连接来执行事务。
1 | /** |
Redis 4.x系列(九):Redis Transaction(事务)