文章目录
- 1.概述
- 2.事务与事务传播
- 2.1 声明式事务说明
- 2.2.声明式事务失效原因
- 2.3.事务的传播机制
- 2.4.事务传播失效原因
- 3.事务使用建议
- 4.总结
1.概述
我们在开发工作中经常会使用到事务,来保证数据库做增、删、改操作时的数据一致性,在使用Spring来处理事务的时候,如果没有正确的使用就很易容出现事务失效或者事务传播失效的问题,导致预期结果与实际结果不符。
为了彻底解决这种“一不小心”出现的事务失效问题,今天就结合理论和代码实践,验证一下什么情况下会导致事务失效,并总结如何正确的使用事务。
注:本篇验证的事务机制为使用@Transactional
注解的声明式事务。
2.事务与事务传播
在做验证之前,需要对Spring中两个容易混淆的概念:事务和事务传播。
- 事务:一种保持数据一致性的机制,具有
ACID
的特征。 - 事务传播:Spring对不同方法的声明式事务组合起来使用,根据组合方式的不同,得到不一样的结果。
2.1 声明式事务说明
下面会简单的说明一下Spring中声明式事务的实现原理,不过在这之前先了解一下编程式事务,这是声明式事务的基础,下面是一个简化的Demo代码:
@Autowired
private PlatformTransactionManager transactionManager;
public void performSomeBusinessLogic() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 执行业务逻辑
// 事务提交
transactionManager.commit(status);
} catch (Exception ex) {
// 事务回滚
transactionManager.rollback(status);
throw ex;
}
}
我们可以使用PlatformTransactionManager
、TransactionDefinition
、TransactionStatus
这3个API完成编程式事务。
对于声明式事务来说,我们通过给方法打上一个@Transactional
注解,就可以让这个方法以事务的方式来运行,而这种方式在Spring中往往是通过AOP
来实现的,将@Transactional
注解作为切点,进入一个处理事务的切面,这个切面里面要做的事就是开启事务,执行业务逻辑,判断事务是提交还是回滚。可以简单的理解为编程式事务那一套代码放到了通过AOP生成代理对象中了。
2.2.声明式事务失效原因
既然是代理对象实现的,那就可以得出一个结论:如果AOP
失效,那么声明式事务也会失效,于是我们可以发现事务失效的几个原因:
- 被声明的方法是非
public
方法、final
方法、静态
方法 - 对象内部
自调用
对第2点,假设当前对象中有两个方法分别是a和b
,a
上面没有写@Transactional
,b
上面写了,此时在a
方法中使用this.b()
来发起调用,这种情况下调用的是b
的实例方法,并不是代理对象中的方法,没有代理自然事务就失效了,这也是下面会提到的事务传播失效的原因。
如果不想把两个方法放到两个不同的类中,可以在a
方法中通过applicationContext.getBean(this.getClass());
再获取一次代理对象,这样就不会失效了。
除此之外,再看一下上面的代码,事务会在catch
块中调用rollback
方法进行回滚,如果在业务流程中处理了异常没有向上抛出到代理对象中,就不会触发rollback
,导致事务的回滚失效,因此我们可以得出事务失效的另一个原因:
- 异常被业务代码逻辑捕获,
没有向上抛出
到代理对象中
针对这个失效原因,需要补充一点的是,在某些场景下我们可能会通过切面对Service
层通过AOP
做统一异常处理,这种情况也可能会导致异常没有抛出到事务相关的代理对象中,导致回滚失效,这种情况隐藏的比较深,可能不容易排查到。
除了AOP
层面导致的事务失效以外,我们再把视线集中的@Transactional
本身上面,在这个注解有两个重要的配置,事务传播配置(Propagation) 和 回滚异常配置(rollbackFor),这两个配置错误也可能导致事务失效。
- 配置了
不支持事务
的事务传播类型 - 需要回滚的
异常类型未正确配置
对与第4点,下面的内容还会详细描述,这里说明一下回滚异常配置。
在Transactional
这个注解类中,对于rollbackFor有这么一段注释:
意思是说,未显式的指定回滚异常时,只要在抛出的异常为RuntimeException
或者抛出的是Error
时才会回滚,而其他的受检异常是不会回滚的。
最后,就是数据库本身的问题:
- 数据库引擎不支持事务(废话)
2.3.事务的传播机制
事务传播是多个带有事务的方法在组合使用时,希望通过配置不同的传播机制来达到不同的结果,Spring中定义的事务传播的配置一共有7种,分别是:
REQUIRED
:多个方法在同一个事务下运行,其中有任一报错,都会一起回滚。REQUIRED_NEW
:调用时会新开一个事务,则多个方法在不同事务中运行,各自的方法报错只会回滚自己,互相不影响。SUPPORT
:被调用的方法会加入调用者的事务,如果调用者没有事务,则非事务运行。NOT_SUPPORTED
:如果调用者有事务,则暂停该事务,被调用方法以非事务运行。MANDATORY
:强制必须有事务,如果没有则抛出异常。NESTED
:多个方法属于同一个事务,但被调用方法的事务属于调用者的子事务。NEVER
:强制必须没有事务,如果有则抛出异常。
这里主要说一下NESTED
,这是一种嵌套事务,与REQUIRED不
同的是,被调用方法报错会将事务回滚到一个savePoint
,不会引起调用者事务回滚,与REQUIRED_NEW
不同的是,如果调用者出现了异常,也会带着被调用者一起回滚。
另外,在项目中使用事务传播机制,还得考虑一下数据库的隔离等级,两者配合很可能会给你带来死锁大礼包,举个简单的例子:用REQUIRED_NEW
做事务传播,调用者和被调用者都update
同一行数据使用id
作为条件,调用者再等待被调用者执行完毕,被调用者再等待调用者完成事务,互相等待导致死锁。
2.4.事务传播失效原因
其实在上面事务失效中已经提到了,事务失效肯定传播也会失效,但这里需要强调的是。最容易忽略的原因还是因为使用的对象的自调用。
我们在编写项目的时候,很多情况下两个方法就是放在同一个service
对象中的,不经意间就通过this.xxx
进行调用了,然后一看就发现事务传播失效了。
3.事务使用建议
在条件允许的情况下,通过编程式事务来替换声明式事务可以有效的避免事务失效,并且可以更细粒度的控制事务,但缺点也比较明显,在业务逻辑中侵入了太多非业务逻辑的代码。
如果一定要使用声明式事务,根据上面提到的一些原因总结一下正确使用事务的几条建议:
- 使用
public
权限来定义方法,不要static
,不要final
- 异常一定要
向上抛出
,即使逻辑内部要做异常处理,也需要抛出一个自定义异常 显式的配置
Transactional中的事务传播和回调异常,不要使用默认配置- 只要涉及到多个事务方法的调用,要注意
自调用
问题,一定要使用代理对象来调用
4.总结
本篇主要是描述了Spring的事务实践中导致事务失效的问题以及正确使用事务的建议,最后将事务失败的原因罗列在这里,方便后续查阅:
- 被声明的方法是非
public
方法、final
方法、静态
方法 - 对象内部
自调用
- 异常被业务代码逻辑捕获,
没有向上抛出
到代理对象中 - 配置了
不支持事务
的事务传播类型 - 需要回滚的
异常类型未正确配置
- 数据库引擎不支持事务(废话)