Spring声明式事务注意事项

[toc]

事务失效

事务失效我们一般要从两个方面排查问题

数据库层面

数据库层面,数据库使用的存储引擎是否支持事务?默认情况下MySQL数据库使用的是Innodb存储引擎(5.5版本之后),它是支持事务的,但是如果你的表特地修改了存储引擎,例如,你通过下面的语句修改了表使用的存储引擎为MyISAM,而MyISAM又是不支持事务的

1
2
alter table table_name engine=myisam;
复制代码

这样就会出现“事务失效”的问题了

解决方案:修改存储引擎为Innodb。

业务代码层面

业务层面的代码是否有问题,这就有很多种可能了

  1. 我们要使用Spring的声明式事务,那么需要执行事务的Bean是否已经交由了Spring管理?在代码中的体现就是类上是否有@Service、Component等一系列注解

解决方案:将Bean交由Spring进行管理(添加@Service注解)

  1. @Transactional注解是否被放在了合适的位置。默认情况下你无法使用@Transactional对一个非public的方法进行事务管理

解决方案:修改需要事务管理的方法为public。

  1. 出现了自调用。什么是自调用呢?我们看个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class DmzService {

public void saveAB(A a, B b) {
saveA(a);
saveB(b);
}

@Transactional
public void saveA(A a) {
dao.saveA(a);
}

@Transactional
public void saveB(B b){
dao.saveB(a);
}
}

上面三个方法都在同一个类DmzService中,其中saveAB方法中调用了本类中的saveA跟saveB方法,这就是自调用。在上面的例子中saveA跟saveB上的事务会失效

那么自调用为什么会导致事务失效呢?我们知道Spring中事务的实现是依赖于AOP的,当容器在创建dmzService这个Bean时,发现这个类中存在了被@Transactional标注的方法(修饰符为public)那么就需要为这个类创建一个代理对象并放入到容器中,创建的代理对象等价于下面这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class DmzServiceProxy {

private DmzService dmzService;

public DmzServiceProxy(DmzService dmzService) {
this.dmzService = dmzService;
}

public void saveAB(A a, B b) {
dmzService.saveAB(a, b);
}

public void saveA(A a) {
try {
// 开启事务
startTransaction();
dmzService.saveA(a);
} catch (Exception e) {
// 出现异常回滚事务
rollbackTransaction();
}
// 提交事务
commitTransaction();
}

public void saveB(B b) {
try {
// 开启事务
startTransaction();
dmzService.saveB(b);
} catch (Exception e) {
// 出现异常回滚事务
rollbackTransaction();
}
// 提交事务
commitTransaction();
}
}

上面是一段伪代码,通过startTransaction、rollbackTransaction、commitTransaction这三个方法模拟代理类实现的逻辑。因为目标类DmzService中的saveA跟saveB方法上存在@Transactional注解,所以会对这两个方法进行拦截并嵌入事务管理的逻辑,同时saveAB方法上没有@Transactional,相当于代理类直接调用了目标类中的方法。

我们会发现当通过代理类调用saveAB时整个方法的调用链如下:

聊一聊使用事务(@Transactional)可能出现的问题

实际上我们在调用saveA跟saveB时调用的是目标类中的方法,这种情况下,事务当然会失效。

解决方案

  1. 自己注入自己,然后显示的调用,例如:

聊一聊使用事务(@Transactional)可能出现的问题

这种方案看起来不是很优雅

  1. 利用AopContext,如下:

聊一聊使用事务(@Transactional)可能出现的问题

使用上面这种解决方案需要注意的是,需要在配置类上新增一个配置

1
2
// exposeProxy=true代表将代理类放入到线程上下文中,默认是false
@EnableAspectJAutoProxy(exposeProxy = true)

总结

聊一聊使用事务(@Transactional)可能出现的问题


事务回滚相关问题

回滚相关的问题可以被总结为两句话

  1. 想回滚的时候事务却提交了
  2. 想提交的时候被标记成只能回滚了(rollback only)

先看第一种情况:想回滚的时候事务却提交了。这种情况往往是程序员对Spring中事务的rollbackFor属性不够了解导致的。

Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务,已经执行的SQL会提交掉。

如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。

对应代码其实我们上篇文章也分析过了,如下:

聊一聊使用事务(@Transactional)可能出现的问题

以上代码位于:TransactionAspectSupport#completeTransactionAfterThrowing方法中

默认情况下,只有出现RuntimeException或者Error才会回滚

1
2
3
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}

所以,如果你现在出现了非RuntimeException或者Error时也回滚,请指定回滚时的异常,例如:

1
@Transactional(rollbackFor = Exception.class)

第二种情况:想提交的时候被标记成只能回滚了(rollback only)

对应的异常信息如下:

1
Transaction rolled back because it has been marked as rollback-only

先来看个例子吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service
public class DmzService {

@Autowired
IndexService indexService;

@Transactional
public void testRollbackOnly() {
try {
indexService.a();
} catch (ClassNotFoundException e) {
System.out.println("catch");
}
// do other thing
}
}

@Service
public class IndexService {
@Transactional(rollbackFor = Exception.class)
public void a() throws ClassNotFoundException{
// ......
throw new ClassNotFoundException();
}
}

在上面这个例子中,DmzService的testRollbackOnly方法跟IndexService的a方法都开启了事务,并且事务的传播级别为required,所以当我们在testRollbackOnly中调用IndexService的a方法时这两个方法应当是共用的一个事务。按照这种思路,虽然IndexService的a方法抛出了异常,但是我们在testRollbackOnly将异常捕获了,那么这个事务应该是可以正常提交的,为什么会抛出异常呢?

源码分在处理回滚时有这么一段代码

聊一聊使用事务(@Transactional)可能出现的问题

在提交时又做了下面这个判断(这个方法我删掉了一些不重要的代码

聊一聊使用事务(@Transactional)可能出现的问题

可以看到当提交时发现事务已经被标记为rollbackOnly后会进入回滚处理中,并且unexpected传入的为true。在处理回滚时又有下面这段代码

聊一聊使用事务(@Transactional)可能出现的问题

最后在这里抛出了这个异常。

以上代码均位于AbstractPlatformTransactionManager中

总结起来,主要的原因就是因为内部事务回滚时将整个大事务做了一个rollbackOnly的标记,所以即使我们在外部事务中catch了抛出的异常,整个事务仍然无法正常提交,并且如果你希望正常提交,Spring还会抛出一个异常。

解决方案:

这个解决方案要依赖业务而定,你要明确你想要的结果是什么

  1. 内部事务发生异常,外部事务catch异常后,内部事务自行回滚,不影响外部事务

聊一聊使用事务(@Transactional)可能出现的问题

虽然这两者都能得到上面的结果,但是它们之间还是有不同的。

当传播级别为requires_new时,两个事务完全没有联系,各自都有自己的事务管理机制(开启事务、关闭事务、回滚事务)。

但是传播级别为nested时,实际上只存在一个事务,只是在调用a方法时设置了一个保存点,当a方法回滚时,实际上是回滚到保存点上,并且当外部事务提交时,内部事务才会提交,外部事务如果回滚,内部事务会跟着回滚。

  1. 内部事务发生异常时,外部事务catch异常后,内外两个事务都回滚,但是方法不抛出异常

聊一聊使用事务(@Transactional)可能出现的问题

通过显示的设置事务的状态为RollbackOnly。这样当提交事务时会进入下面这段代码

聊一聊使用事务(@Transactional)可能出现的问题

最大的区别在于处理回滚时第二个参数传入的是false,这意味着回滚是预期之中的,所以在处理完回滚后并不会抛出异常。



1、@Transactional 应用在非 public 修饰的方法上

如果Transactional注解应用在非public 修饰的方法上,Transactional将会失效。

一口气说出6种,@Transactional注解的失效场景

之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。

1
2
3
4
5
6
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}

此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

2、@Transactional 注解属性 propagation 设置错误

这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

3、@Transactional 注解属性 rollbackFor 设置错误

rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。

一口气说出6种,@Transactional注解的失效场景

1
2
// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class

若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。Spring源码如下:

1
2
3
4
5
6
7
8
9
10
11
private int getDepth(Class<?> exceptionClass, int depth) {
if (exceptionClass.getName().contains(this.exceptionName)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionClass == Throwable.class) {
return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}

4、同一个类中方法调用,导致@Transactional失效

开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。

那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
/**
* B 插入字段为 3的数据
*/
this.insertB();
/**
* A 插入字段为 2的数据
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);

return insert;
}

@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);

return cityInfoDictMapper.insert(cityInfoDict);
}

5、异常被你的 catch“吃了”导致@Transactional失效

这种情况是最常见的一种@Transactional注解失效场景,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段为 3的数据
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}

如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?

答案:不能!

会抛出异常:

1
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

因为当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。

spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。

在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致,所以有些时候try catch反倒会画蛇添足。

6、数据库引擎不支持事务

这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。

转载自:

https://juejin.im/post/5e72e97c6fb9a07cb346083f