使用@Transactional注解事务,可能出现哪些问题呢
前言
在之前的文章中已经对Spring中的事务做了详细的分析了,这篇文章我们来聊一聊平常工作时使用事务可能出现的一些问题(本文主要针对使用@Transactional进行事务管理的方式进行讨论)以及对应的解决方案
- 事务失效
- 事务回答相关问题
- 读写分离跟事务结合使用时的问题
事务失效
事务失效我们一般要从两个方面排查问题
- 数据库层面
数据库层面,数据库使用的存储引擎是否支持事务?默认情况下MySQL数据库使用的是Innodb存储引擎(5.5版本之后),它是支持事务的,但是如果你的表特地修改了存储引擎,例如,你通过下面的语句修改了表使用的存储引擎为MyISAM,而MyISAM又是不支持事务的
alter table table_name engine=myisam;
这样就会出现“事务失效”的问题了
解决方案:修改存储引擎为Innodb。
- 业务代码层面
业务层面的代码是否有问题,这就有很多种可能了
我们要使用Spring的声明式事务,那么需要执行事务的Bean是否已经交由了Spring管理?在代码中的体现就是类上是否有@Service、Component等一系列注解
解决方案:将Bean交由Spring进行管理(添加@Service注解)
@Transactional注解是否被放在了合适的位置。在上篇文章中我们对Spring中事务失效的原理做了详细的分析,其中也分析了Spring内部是如何解析@Transactional注解的,我们稍微回顾下代码:
也就是说,默认情况下你无法使用@Transactional对一个非public的方法进行事务管理
解决方案:修改需要事务管理的方法为public。
出现了自调用。什么是自调用呢?我们看个例子
@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)那么就需要为这个类创建一个代理对象并放入到容器中,创建的代理对象等价于下面这个类
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时整个方法的调用链如下:
实际上我们在调用saveA跟saveB时调用的是目标类中的方法,这种情况下,事务当然会失效。
常见的自调用导致的事务失效还有一个例子,如下:
@Service
public class DmzService {
@Transactional
public void save(A a, B b) {
saveB(b); } @Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB(B b){
dao.saveB(a);
}
}
当我们调用save方法时,我们预期的执行流程是这样的
也就是说两个事务之间互不干扰,每个事务都有自己的开启、回滚、提交操作。
但根据之前的分析我们知道,实际上在调用saveB方法时,是直接调用的目标类中的saveB方法,在saveB方法前后并不会有事务的开启或者提交、回滚等操作,实际的流程是下面这样的
由于saveB方法实际上是由dmzService也就是目标类自己调用的,所以在saveB方法的前后并不会执行事务的相关操作。这也是自调用带来问题的根本原因:自调用时,调用的是目标类中的方法而不是代理类中的方法
解决方案
自己注入自己,然后显示的调用,例如:
这种方案看起来不是很优雅
利用AopContext,如下:
使用上面这种解决方案需要注意的是,需要在配置类上新增一个配置
// exposeProxy=true代表将代理类放入到线程上下文中,默认是false
@EnableAspectJAutoProxy(exposeProxy = true)
写在最后
通过一张图总结事务失败的原因
如果觉得可以帮到你,记得关注我哦O(∩_∩)O哈哈~
上一篇: RocketMq 事务消息使用
下一篇: 事务特征的四个特征和事务并发问题(面试)