本文共xxxx字,将列举Spring事务失效的几种实际场景,帮助大家避免和发现此类问题,并保证事务的正常启用。
前言
Spring可谓是目前最流行的Java开发框架了,除了为开发者提供便利和强大的开发方式外,它也整合了数据库的事务功能,形成了一套事务管理的框架。
一般情况下,在SpringBoot强大的注解模式下,我们都是采用@Transaction的注解进行事务在方法层面的开启。
但很多情况下,会发现,咦,自己明明配置了注解,也启动了配置,为何事务不生效呢?
下面,我们就来列举下常见的几种事务失效场景。
事务失效场景总结
当使用的数据库引擎不支持事务的时候,那么Spring即使开启了事务,也不会生效,要知道,Spring的事务管理实际上是对数据库事务的一次封装。
这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。
从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么搞都是白搭。
当程序引用的数据源,没有配置事务管理器时,便相当于没有事务管理,自然也不会生效
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
Spring官方表示:@Transaction注解需要用在public方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启AspectJ的代理模式。
也就是说使用了@Transaction注解的方法,其所在的类没有被Spring创建为一个Bean,此时是不会被Spring所管理的,那么事务自然也就失效了。
之前有一篇文章讲述了事务传播机制,其中有不支持事务的传播机制,比如说NOT_SUPPORTED。也就是说,当我们在@Transaction注解中,使用了不支持事务的传播机制时,此时事务是不会生效的。
比如:
@Service
public class TestServiceImpl implements TestService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void testTran(String arg) {
// doSomething
}
}
可见,当配置属性propagation为Propagation.NOT_SUPPORTED时,表示主动不以事务运行。
Spring的事务管理,实际上就是通过代理,在配置了@Transaction的方法前后拦截,在捕获到对应类型的异常时进行回滚。那么如果我们在方法里进行手动的try..catch,并且将捕获到的异常给吃了,那么Spring代理便无法得知方法调用的异常情况,就无法进行事务异常时的回滚操作。
比如:
@Service
public class TestServiceImpl implements TestService {
@Transactional
public void testTran(String arg) {
try{
// doSomething
} catch (Exception e){
// 捕获处理不抛出
}
}
Spring的事务异常回滚,有自己制定的异常类型,当满足了条件才会进行回滚,默认的异常类型为RuntimeException,那么此时如果我们抛出了非这个类型的异常时,同样是不会进行回滚的。
比如:
@Service
public class TestServiceImpl implements TestService {
@Transactional
public void testTran(String arg) {
try{
// doSomething
} catch (Exception e){
throw new Exception(....)
}
}
那么如何解决这个问题呢,我们可以通过配置@Transaction里的rollbackFor属性,指定为Exception.class来解决问题,但建议针对业务性异常,最好自己在程序里创建继承于RuntimeException的异常类,并显式抛出。
来看下下面这个代码:
@Service
public class TestServiceImpl implements TestService {
public void test(String arg){
this.testTran(arg);
}
@Transactional
public void testTran(String arg) {
try{
// doSomething
} catch (Exception e){
throw new RuntimeException(....)
}
}
当我们调用test()这个没有加事务的方法的时候,其调用的testTran()方法若是出现了异常,此时testTran()所启用的事务会回滚吗?
答案是不会的。
再看下面这个代码:
@Service
public class TestServiceImpl implements TestService {
@Transactional
public void test(String arg){
this.testTran(arg);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void testTran(String arg) {
try{
// doSomething
} catch (Exception e){
throw new RuntimeException(....)
}
}
我们为test()方法开启事务,并且对其调用的方法,我们配置传播属性为——创建一个新事务。
此时这个新开的事务会生效吗?
答案也是不会的。
之所以不会,是因为它们发生了自身调用。这个问题可谓是许多人都会碰到的一种失效情况,大多数方法,我们会出现调用的情况,当调用了自身类的方法时,由于此时没有经过Spring的代理类,也就是内部调用了事务方法,这种情况将导致该调用方法的事务失效。
那么如何解决自身调用出现的事务失效问题呢?
- 一种是在类中再注入自己,调用时通过注入的这个bean来进行方法调用,此时会由Spring的代理类进行方法调用,事务将生效,缺点是不优雅。
- 一种是将调用方法写到另一个bean中,注入该bean再去调用,缺点无疑是将相同业务的代码拆分,导致复杂度增高。
- 一种是通过AopContext.currentProxy()来获取当前代理,转换为当前类后进行方法调用。比如
((TestService)AopContext.currentProxy()).testTran(arg);
这种方式是官方为我们提出的解决方案,也建议使用这种方案,注意,使用AopContext.currentProxy()是需要将expose-proxy设置为true才能生效的。
Tip
以上列举出来了8种常见的事务失效场景和部分解决方案,其中最常见的便是“异常不抛出”、“异常类型不正确”以及“自身调用”这三个问题。