使用事务时(@Transactional)可能出现问题?我完美避开了

代码位于:AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

也就是说,默认情况下你无法使用@Transactional对一个非public的方法进行事务管理

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

  1. 出现了自调用。什么是自调用呢?我们看个例子

@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方法中调用了本类中的saveAsaveB方法,这就是自调用。在上面的例子中saveAsaveB上的事务会失效

那么自调用为什么会导致事务失效呢?我们知道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();
}
}

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

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

实际上我们在调用saveAsaveB时调用的是目标类中的方法,这种清空下,事务当然会失效。

常见的自调用导致的事务失效还有一个例子,如下:

@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方法的前后并不会执行事务的相关操作。这也是自调用带来问题的根本原因:自调用时,调用的是目标类中的方法而不是代理类中的方法

解决方案

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

@Service
public class DmzService {
// 自己注入自己
@Autowired
DmzService dmzService;

@Transactional
public void save(A a, B b) {
dmzService.saveB(b);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB(B b){
dao.saveB(a);
}
}

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

  1. 利用AopContext,如下:

@Service
public class DmzService {

@Transactional
public void save(A a, B b) {
((DmzService) AopContext.currentProxy()).saveB(b);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB(B b){
dao.saveB(a);
}
}

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

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

个人比较喜欢的是第二种方式

这里我们做个来做个小总结

总结

一图胜千言

事务回滚相关问题

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

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

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

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

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

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

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

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

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

@Transactional(rollbackFor = Exception.class)

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

对应的异常信息如下:

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

我们先来看个例子吧

@Service
public class DmzService {

@Autowired
IndexService indexService;

@Transactional
public void testRollbackOnly() {
try {
indexService.a();
} catch (ClassNotFoundException e) {
System.out.println(“catch”);
}
}
}

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

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

如果你看过我之前的源码分析的文章应该知道,在处理回滚时有这么一段代码

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我还通过一些渠道整理了一些大厂真实面试主要有:蚂蚁金服、拼多多、阿里云、百度、唯品会、携程、丰巢科技、乐信、软通动力、OPPO、银盛支付、中国平安等初,中级,高级Java面试题集合,附带超详细答案,希望能帮助到大家。

新鲜出炉的蚂蚁金服面经,熬夜整理出来的答案,已有千人收藏

还有专门针对JVM、SPringBoot、SpringCloud、数据库、Linux、缓存、消息中间件、源码等相关面试题。

新鲜出炉的蚂蚁金服面经,熬夜整理出来的答案,已有千人收藏

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
望能帮助到大家。**

[外链图片转存中…(img-IJqt2FLB-1713525229290)]

还有专门针对JVM、SPringBoot、SpringCloud、数据库、Linux、缓存、消息中间件、源码等相关面试题。

[外链图片转存中…(img-1GzZXR0D-1713525229290)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值