事务大家平时应该都有写,之前写事务的时候遇到一点坑,居然不生效,后来排查了一下,复习了一下各种事务失效的场景,想着不如来一个总结,这样下次排查问题,就能有恃无恐了。那么先来复习一下事务相关知识,事务是指操作的最小工作单位,作为一个单独且不可切割的单元操作,要么全部成功,要么全部失败。事务有四大特性(<code>ACID</code>):
原子性(<code>Atomicity</code>):事务包含的操作,要么全部成功,要么全部失败回滚,不会存在一半成功一半失败的中间状态。比如<code>A</code>和<code>B</code>一开始都有<code>500</code>元,<code>A</code>给<code>B</code>转账<code>100</code>,那么<code>A</code>的钱少了<code>100</code>,<code>B</code>的钱就必须多了<code>100</code>,不能<code>A</code>少了钱,<code>B</code>也没收到钱,那这个钱就不翼而飞了,不符合原子性了。
一致性(<code>Consistency</code>):一致性是指事务执行之前和之后,保持整体状态的一致,比如<code>A</code>和<code>B</code>一开始都有<code>500</code>元,加起来是<code>1000</code>元,这个是之前的状态,<code>A</code>给<code>B</code>转账<code>100</code>,那么最后<code>A</code>是<code>400</code>,<code>B</code>是<code>600</code>,两者加起来还是<code>1000</code>,这个整体状态需要保证。
隔离性(<code>Isolation</code>):前面两个特性都是针对同一个事务的,而隔离性指的是不同的事务,当多个事务同时在操作同一个数据的时候,需要隔离不同事务之间的影响,并发执行的事务之间不能相互干扰。
持久性(<code>Durability</code>):指事务如果一旦被提交了,那么对数据库的修改就是永久性的,就算是数据库发生故障了,已经发生的修改也必然存在。
事务的几个特性并不是数据库事务专属的,广义上的事务是一种工作机制,是并发控制的基本单位,保证操作的结果,还会包括分布式事务之类的,但是一般我们谈论事务,不特指的话,说的就是与数据库相关的,因为我们平时说的事务基本都基于数据库来完成。
事务不仅是适用于数据库。我们可以将此概念扩展到其他组件,类似队列服务或外部系统状态。因此,“一系列数据操作语句必须完全完成或完全失败,以一致的状态离开系统”
前面我们已经部署过了一些demo项目,以及用docker快速搭建环境,本文基于的也是之前的环境:
JDK 1.8
Maven 3.6
Docker
Mysql
正常的事务样例,包含两个接口,一个是获取所有的用户中的数据,另外一个更新的,是<code>update</code>用户数据,其实就是每个用户的年龄<code>+1</code>,我们让一次操作完第一个之后,抛出异常,看看最后的结果:
数据库操作:
先获取<code>http://localhost:8081/getUserList</code>所有的用户看看:

在调用更新接口,页面抛出错误了:
控制台也出现了异常,意思是除以0,异常:
然后我们再次请求<code>http://localhost:8081/getUserList</code>,看到数据两个都是<code>11</code>说明数据都没有发生变化,第一个操作完之后,异常,回滚成功了:
那什么时候事务不正常回滚呢?且听我细细道来:
我们知道,<code>Mysql</code>其实有一个数据库引擎的概念,我们可以用<code>show engines</code>来查看<code>Mysql</code>支持的数据引擎:
可以看到<code>Transactions</code>那一列,也就是事务支持,只有<code>InnoDB</code>,那就是只有<code>InnoDB</code>支持事务,所以要是引擎设置成其他的事务会无效。
我们可以用<code>show variables like 'default_storage_engine'</code>看默认的数据库引擎,可以看到默认是<code>InnoDB</code>:
那我们看看我们演示的数据表是不是也是用了<code>InnoDB</code>,可以看到确实是使用<code>InnoDB</code>
那我们把该表的引擎修改成<code>MyISAM</code>会怎么样呢?试试,在这里我们只修改数据表的数据引擎:
然后再<code>update</code>,不出意料,还是会报错,看起来错误没有什么不同:
但是获取全部数据的时候,第一个数据更新成功了,第二个数据没有更新成功,说明事务没有生效。
结论:必须设置为<code>InnoDB</code>引擎,事务才生效。
事务必须是<code>public</code>方法,如果用在了<code>private</code>方法上,那么事务会自动失效,但是在<code>IDEA</code>中,只要我们写了就会报错:<code>Methods annotated with '@Transactional' must be overrideable</code>,意思是事务的注解加上的方法,必须是可以重写的,<code>private</code>方法是不可以重写的,所以报错了。
同样的<code>final</code>修饰的方法,如果加上了注解,也会报错,因为用<code>final</code>就是不想被重写:
<code>Spring</code>中主要是用放射获取<code>Bean</code>的注解信息,然后利用基于动态代理技术的<code>AOP</code>来封装了整个事务,理论上我想调用<code>private</code>方法也是没有问题的,在方法级别使用<code>method.setAccessible(true);</code>就可以,但是可能<code>Spring</code>团队觉得<code>private</code>方法就是开发人员意愿上不愿意公开的接口,没有必要破坏封装性,这样容易导致混乱。
<code>Protected</code>方法可不可以?不可以!
下面我们为了实现,魔改代码结构,因为接口不能用<code>Portected</code>,如果用了接口,就不可能用<code>protected</code>方法,会直接报错,而且必须在同一个包里面使用,我们把<code>controller</code>和<code>service</code>放到同一个包下:
测试后发现事务不生效,结果依然是一个更新了,另外一个没有更新:
结论:必须使用在<code>public</code>方法上,不能用在<code>private</code>,<code>final</code>,<code>static</code>方法上,否则不会生效。
<code>Springboot</code>管理异常的时候,只会对运行时的异常(<code>RuntimeException</code> 以及它的子类) 进行回滚,比如我们前面写的<code>i=1/0;</code>,就会产生运行时的异常。
从源码来看也可以看到,<code>rollbackOn(ex)</code>方法会判断异常是<code>RuntimeException</code>或者<code>Error</code>:
异常主要分为以下类型:
所有的异常都是<code>Throwable</code>,而<code>Error</code>是错误信息,一般是程序发生了一些不可控的错误,比如没有这个文件,内存溢出,<code>IO</code>突然错误了。而<code>Exception</code>下,除了<code>RuntimeException</code>,其他的都是<code>CheckException</code>,也就是可以处理的异常,<code>Java</code>程序在编写的时候就必须处理这个异常,否则编译是通不过去的。
由下面的图我们可以看出,<code>CheckedException</code>,我列举了几个常见的<code>IOException</code> IO异常,<code>NoSuchMethodException</code>没有找到这个方法,<code>ClassNotFoundException</code> 没找到这个类,而<code>RunTimeException</code>有常见的几种:
数组越界异常:<code>IndexOutOfBoundsException</code>
类型转换异常:<code>ClassCastException</code>
空指针异常:<code>NullPointerException</code>
事务默认回滚的是:运行时异常,也就是<code>RunTimeException</code>,如果抛出其他的异常是无法回滚的,比如下面的代码,事务就会失效:
方法上需要使用<code>@Transactional</code>才能开启事务
多个数据源配置或者多个事务管理器的时候,注意如果操作数据库<code>A</code>,不能使用<code>B</code>的事务,虽然这个问题很幼稚,但是有时候用错难查找问题。
如果在<code>Spring</code>中,需要配置<code>@EnableTransactionManagement</code>来开启事务,等同于配置<code>xml</code>文件<code>*<tx:annotation-driven/>*</code>,但是在<code>Springboot</code>中已经不需要了,在<code>springboot</code>中<code>SpringBootApplication</code>注解包含了<code>@EnableAutoConfiguration</code>注解,会自动注入。
<code>@EnableAutoConfiguration</code>自动注入了哪些东西呢?在<code>jetbrains://idea/navigate/reference?project=springDocker&path=~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.6/spring-boot-autoconfigure-2.5.6.jar!/META-INF/spring.factories</code>下有自动注入的配置:
里面配置了一个<code>TransactionAutoConfiguration</code>,这是事务自动配置类:
值得注意的是,<code>@Transactional</code>除了可以用于方法,还可以用于类,表示这个类所有的<code>public</code>方法都会配置事务。
想要进行事务管理的方法只能在其他类里面被调用,不能在当前类被调用,否则会失效,为了实现这个目的,如果同一个类有不少事务方法,还有其他方法,这个时候有必要抽取出一个事务类,这样分层会比较清晰,避免后继者写的时候在同一个类调用事务方法,造成混乱。
事务失效的例子:
比如我们将<code>service</code>事务方法改成:
在<code>controller</code>里面调用的是没有事务注解的方法,再间接调用事务方法:
调用之后,发现事务失效,一个更新另外一个没有更新:
为什么会这样呢?
<code>Spring</code>用切面对方法进行包装,只对外部调用方法进行拦截,内部方法没有进行拦截。
看源码:实际上我们调用事务方法的时候,会进入<code>DynamicAdvisedInterceptor</code>的<code>public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)()</code>方法:
里面调用了<code>AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()</code>,这里是获取调用调用链。而没有<code>@Transactional</code>注解的方法<code>userService.testTransaction()</code>,根本获取不到代理调用链,调用的还是原来的类的方法。
<code>spring</code>里面要想对一个方法进行代理,用的就是<code>aop</code>,肯定需要一个标识,标识哪一个方法或者类需要被代理,<code>spring</code>里面定义了<code>@Transactional</code>作为切点,我们定义这个标识,就会被代理。
代理的时机是什么时候呢?
<code>Spring</code>统一管理了我们的<code>bean</code>,代理的时机自然就是创建<code>bean</code>的过程,看看哪一个类带了这个标识,就生成代理对象。
<code>SpringTransactionAnnotationParser</code>这个类有一个方法是用来判断<code>TransactionAttribute</code>注解的:
假设我们在多线程里面像以下方式使用事务,那么事务是不能正常回滚的:
因为不同的线程使用的是不同<code>SqlSession</code>,相当于另外一个连接,根本不会用到同一个事务:
首先事务是有传播机制的:
<code>REQUIRED</code>(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务,如果有直接使用当前的事务。
<code>SUPPORTS</code>:支持使用当前事务,如果当前事务不存在,就不会使用事务。
<code>MANDATORY</code>:支持使用当前事务,如果当前事务不存在,则抛出<code>Exception</code>,也就是必须当前处于事务里面。
<code>REQUIRES_NEW</code>:创建新事务,如果当前事务存在,把当前事务挂起。
<code>NOT_SUPPORTED</code>:没有事务执行,如果当前事务存在,把当前事务挂起。
<code>NEVER</code>:没有事务执行,如果当前有事务则抛出<code>Exception</code>。
<code>NESTED</code>:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟`REQUIRED
查不多。
默认的是<code>REQUIRED</code>,也就是事务里面调用另外的事务,实际上不会重新创建事务,而是会重用当前的事务。那如果我们这样来写嵌套事务:
调用的另外一个事务:
会抛出以下错误:
我们但是实际事务是正常回滚掉了,结果是对的,之所以出现这个问题,是因为里面到方法抛出了异常,用的是同一个事务,说明事务必须被回滚掉的,但是外层被<code>catch</code>住了,本来就是同一个事务,一个说回滚,一个<code>catch</code>住不让<code>spring</code>感知到<code>Exception</code>,那不是自相矛盾么?所以<code>spring</code>报错说:这个事务被标识了必须回滚掉,最终还是回滚掉了。
怎么处理呢?
外层主动抛出错误,<code>throw new RuntimeException()</code>
使用<code>TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();</code>主动标识回滚
有些时候,我们不仅操作自己的数据库,还需要同时考虑外部的请求,比如同步数据,同步失败,需要回滚掉自己的状态,在这种场景下,必须考虑网络请求是否会出错,出错如何处理,错误码是哪一个的时候才成功。
如果网络超时了,实际上成功了,但是我们判定为没有成功,回滚掉了,可能会导致数据不一致。这种需要被调用方支持重试,重试的时候,需要支持幂等,多次调用保存状态的一致,虽然整个主流程很简单,里面的细节还是比较多的。
事务被<code>Spring</code>包裹了复杂性,很多东西可能源码很深,我们用的时候注意模拟测试一下调用是不是能正常回滚,不能理所当然,人是会出错的,而很多时候黑盒测试根本测试这种异常数据,如果没有正常回滚,后面需要手动处理,考虑到系统之间同步的问题,会造成很多不必要的麻烦,手动改数据库这流程就必须走。
【作者简介】:
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。
剑指Offer全部题解PDF
2020年我写了什么?
开源编程笔记