1.概述

Redis的事务的本质是将一组命令进行打包,然后按照顺序执行命令,并且执行过程不会被其他客户端发送的命令打断插入,执行过程中若出现失败的命令也不会导致事务终止,而是跳过并执行下一条命令。

2.执行过程

事务相关命令:

  • MULTI: 开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
  • EXEC: 执行事务中的所有操作命令。
  • DISCARD: 取消事务,放弃执行事务块中的所有命令。
  • WATCH: 监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH: 取消WATCH对所有key的监视。

图片


Redis事务提供的watch监控,必须在事务开启前执行。这个很容易理解,因为事务一旦开始执行就不会被上下文切换打断,那么执行期间也不可能执行其他命令,也就没有监控的意义。另外考虑到并发问题,事务在执行命令队列前会通过CAS进行检查,一旦发现修改记录,会阻止命令队列的执行。最后watch的监控是一次性的,无论执行是否成功,都会释放监控。

3.客户端命令

RedisTemplate默认不支持事务操作,并且每次执行命令都会从连接池获取资源,使用完毕后归还。如果开启事务需要将enableTransactionSupport属性设置为true,这种情况下会将连接池获取的资源绑定到当前线程,直到事务提交后归还,以减少频繁的获取资源带来的消耗。

1
template.setEnableTransactionSupport(true); // 开启事务支持

如果在@Transactional修饰的方法中使用Redis事务,Spring会自动帮我们开启、提交/回滚事务,在没有被@Transactional修饰的方法中,就需要手动调用API控制事务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// 监控,一定要在事务开启前执行,不然会报错!
redisTemplate.watch("testKey3");

// 开启事务
template..multi();

// 执行命令
redisTemplate.opsForValue().set("testKey1", "abc");
redisTemplate.opsForValue().increment("testKey2");

// 提交事务
List<Object> execResultList = template.exec();

// 如果集合为空,说明watch监控的key被修改过
if(execResultList.isEmpty()){
// 逻辑处理...
}else{
// 逻辑处理...
}

4.与关系型数据库事务的区别

4.1 原子性

原子性是指一个事务内的SQL或命令集是不可分割的,要么都执行要么都不执行。关系型数据库以Mysql为例,事务内的多条SQL执行会先写入redolog中,直到事务提交才会执行磁盘IO进行持久化,如果执行过程中断电、异常、手动回滚事务则不会将SQL的执行效果刷回磁盘,达到要么都执行要么都不执行的效果。

Redis的事务原子性属于弱原子性,我们都知道Redis执行命令是单线程执行,Redis仅仅保证某个事务的命令集执行过程中不会被CPU的上下文切换打断,并且不支持异常回滚,出现异常的命令会跳过继续往下执行。所以说Redis事务不是严格意义上的原子性,因为达不到要么都执行,要么都不执行的效果。

在原子性这方面Redis也增加一些机制减少命令执行错误带来的影响,比如命令总是发送到服务端的命令池里,这样服务端就可以对命令进行一些名称、格式之类的校验,以减少事务执行过程中的异常几率,如果在测试环境前做了充足的测试,出现命令异常的可能性很小,在这种理论前提下,Redis的原子性与关系型数据库也不会有太大差距,但是断电的情况就无法做任何保证。

4.2 一致性

关系型数据库的一致性是事务执行成功后,涉及到的数据从一个一致状态转移到另一个一致状态,但完整性约束没有被破坏,比如经典的转账,先扣钱A账户的钱,在添加B账号的钱,在事务执行完毕后两者的钱总合不应该发生变化(不考虑扣手续费情况),这就是完整性约束,关系型数据的一致性是由其他三大特性和应用程序来保证的,而Redis并不满足原子性和持久性,所以一致性也无法保证。

4.3 隔离性

这个没啥好说的,Redis以单线程模式处理所有命令,并且执行过程中不会被上下文切换打断,因此Redis的事务总是串行化执行,不存在隔离性这种问题。

4.4 持久性

关系型数据库每次事务提交后就会进行持久化,Redis的持久化并不与事务的提交挂钩,因此Redis的事务是没有持久化这个特性的。

5.为什么不支持异常回滚?

通常我们应用程序都是通过各种客户端连接Redis服务,执行命令也都是调用API,最终客户端帮我们生成命令并发送,不会出现语法错误,其次客户端在真正执行事务前也会对命令进行校验,比如对某个HashMap类型的key使用了add,或者对value非数字的String类型key,使用了increment命令,这些服务端都会检测出来并拒绝执行,另外还有watch来提高事务的原子性。再加上开发过程中的测试,几乎不会存在命令执行异常的情况。

对于事务执行过程中出现宕机、断电情况,也不是没法保证,只是这种保证会让整个Redis的设计更加复杂,命令的处理效率变低,而Redis设计的首要目的是让程序快速运行,事务操作会让整个服务阻塞点增多,降低命令处理的吞吐量,为了这种小概率发生的场景提高设计复杂度和性能,似乎不划算。

评论