【前言】 这篇笔记主要用于记录下使用@Transcational注解时,哪些场景会导致事务失效,扫坑排雷。
场景一、未开启事务管理
在Spring应用程序中,要使用@Transactional注解来管理事务,必须要在启动类上添加@EnableTransactionManagement注解。这个注解的作用是开启Spring 的事务管理器,对添加了@Transactional注解的类进行管理。如果没有添加@EnableTransactionManagement注解,则@Transactional注解将不起作用,无法管理事务。
因此,如果在工作中遇到了@Transactional注解失效的问题,可以检查启动类上是否添加了@EnableTransactionManagement注解。如果没有添加,可以在启动类上添加该注解来开启事务管理器。
@SpringBootApplication
@EnableTransactionManagement // 开启事务管理器
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
当使用的是SpringBoot框架时,它自动帮你开启了事务管理。SpringBoot应用的启动类都会加上 @SpringBootApplication 注解,而@SpringBootApplication注解其实是 @EnableAutoConfiguration 注解和其他注解的组合。
在@EnableAutoConfiguration注解中,可以看到引入 AutoConfigurationImportSelector.class 选择器。
直接查看AutoConfigurationImportSelector的源码,在selectImports方法中调用了getAutoConfigurationEntry的方法。
进入方法里,可以看到getCandidateConfigurations的方法
进入该方法中, 可以知道该方法会去读 spring.factories 文件,我们可以从项目的Externnal Libraries 里面找到org.springframework.boot:spring-boot-autoconfigure ,打开 META-INF 文件夹,在spring目录中找到标注的文件:
在文件中可以看到与transaction相关的TransactionAutoConfiguration类:
找到这个类,从相关描述中可以看到,该类与自动装配事务有关:
在这个类里面看到了@EnableTransactionManagement的启用注解:
场景二、数据库引擎不支持事务
这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。
场景三、@Transactional 应用在非 public 修饰的方法上
当@Transactional
注解应用在非public
修饰的方法上时,它将失效。这是因为在Spring AOP代理时,TransactionInterceptor
(事务拦截器)会在目标方法执行前后进行拦截。在这个过程中,拦截器的 intercept
方法会间接调用 AbstractFallbackTransactionAttributeSource
的 computeTransactionAttribute
方法,以获取 Transactional
注解的事务配置信息。
在 computeTransactionAttribute
方法中,会检查目标方法的修饰符是否为 public
。如果不是 public
,则不会获取 @Transactional
的属性配置信息 。
/**
* Same signature as {@link #getTransactionAttribute}, but doesn't cache the result.
* {@link #getTransactionAttribute} is effectively a caching decorator for this method.
* <p>As of 4.1.8, this method can be overridden.
* @since 4.1.8
* @see #getTransactionAttribute
*/
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
...
}
注意:这种情况虽然事务无效,但是也不会报错,这块是一个需要避坑的地方。
场景四、程序中try/catch处理了异常
我们进入@Transactional注解查看其详细内容,在rollback属性中可以看到如下要求:
默认情况下,只有当Error或者RuntimeException发生时,才会触发回滚。所以把异常捕获到,但没有抛出的情况下,事务回滚是无法生效的。
在业务方法中需要事务的一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效
场景五、@Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了非受检unchecked异常(继承自 RuntimeException)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常或该异常的子类时,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。
示例如下:
@Transactional(rollbackFor = Exception.class)
场景六、同一个类中方法调用,导致@Transactional失效(较容易犯)
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B,但方法A没有声明注解事务,而B方法有。外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
原因:其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
因为Spring在扫描bean时会为有注解的方法动态地生成一个子类(即代理类proxy),然后在调用有注解的方法时,实际上是由proxy来调用的,proxy在调用之前就会启动transaction。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过proxy,而是直接通过原来的那个bean,所以就不会启动transaction,即@Transactional注解无效。
场景七、在非Spring容器管理的类中使用@Transactional注解
在非Spring容器管理的类中使用@Transactional注解时,Spring AOP无法拦截到该类中的方法,因此事务管理也无法生效。
场景八、DDL语句无法事务回滚
在MySQL中,DDL语句是非事务的。这意味着DDL语句无法被回滚。如果DDL语句在事务中被执行,将会导致该事务隐式提交,使得该事务中先前执行的DML操作无法回滚。
比如说清表操作,当使用truncate进行清表时,事务无法回滚,改写成delete命令时,可以正常回滚数据:
# DDL 无法回滚
truncate tabelA;
# DML 可以回滚
delete from tabeA;
在MySQL 8.0中,支持在事务中回滚DDL语句。在MySQL 8.0之前的版本,如果DDL语句在事务中被执行,会导致该事务隐式提交。如果在该事务中执行rollback,只有该事务中的DML语句会被回滚,而DDL语句无法回滚。
因此,为了避免DDL语句破坏事务,应该在事务中避免使用DDL语句。如果需要使用DDL语句,应该将DDL语句和DML语句分开执行,或者使用MySQL 8.0及以上版本。