@Transactional失效问题

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

关于@Transactional

日常做项目时,一般情况下Service方法中如果有多个增删改方法的调用,我们会在该业务方法上加@Transactional从而保证事务的执行(SpringBoot自动装配默认开启事务管理,无需@EnableTransactionManagement):

这段代码没太多意义,就是更新一个User的同时,更新另一个。

@Transactional注解有多个属性可以设置,实际开发中比较常用的有两个:

  • propagation:用于指定事务传播行为
  • rollbackFor:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型

这篇文章还不错,可以看完后再回来:总结6种@Transactional注解的失效场景

对于propagation属性,Spring提供了一个枚举类方便我们指定事务传播行为的类型:

特别注意,@Transactional默认的事务传播行为是Propagation.REQUIRED,所以上面的updateUser()我只指定了rollbackFor。

上面文章提到的6种情况里,一般来说可能犯错误的就以下2种:

  • 同一个类中方法调用,导致@Transactional失效
  • 异常被你的catch“吃了”导致@Transactional失效

对于第2种情况,我的处理办法是尽量不在Service层直接try catch,而是习惯抛出业务异常,让@RestControllerAdvise统一捕获并返回给前端。

但对于第1种情况,怎么处理呢?毕竟实际开发中,有时确实可能一不小心就发生同一个类的方法互调,此时如何解决事务失效问题呢?

发现问题

请观察下方截图中的代码,不用在意具体的上下文:

  • selectUser()不加事务控制,但调用了updateUser()
  • updateUser加了事务控制,调用了两次userMapper.update(),中间会抛出“除零异常”

selectUser()不够贴切,名字随便取的,请把它当做一个没有事务的增删改方法

在test方法中调用:

测试前数据库记录:

测试结果:

这证明了同一个类中的非事务方法调用事务方法确实会导致事务失效(如果事务没失效,应该会回滚,16不会被修改)。

解决问题

方法1:给selectUser()加上@Transactional

事务确实控制住了:

方法2:ApplicationContext获取代理对象

同一个类中非事务方法调用事务方法导致事务失效的根本原因在于,非事务方法中调用updateUser()本质上就是this.updateUser(),而this并不是代理对象,而是普通对象(后面再解释)。

知道原因后就很好解决了:

先在selectUser()内部获取UserService的代理对象,再通过代理对象调用updateUser()即可

方法3:注入自身

由于Spring已经替我们解决了循环依赖的问题,所以AService可以注入AService自身。

比如:

@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserService userService
}

方法4:AopContext.currentProxy()获取代理对象

原理同上,本质是也是在selectUser()方法中获取代理对象。不过这个方法需要额外做2步:

  • 引入aop依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 添加注解

AopContext可以通过当前线程ThreadLocal得到代理对象。

关于代理对象与this

最后分别解释一下上面三种办法为什么能解决事务失效的问题,其中方法2和3的原理是一样的。

先看方法1:给selectUser()加上@Transactional

我们原先观察问题的角度是:selectUser()调用updateUser(),会导致updateUser()事务失效。一般来说,正向思维是想办法让updateUser()事务起效,但方法1却采用了逆向思维:让selectUser()的事务起效,从而把updateUser()放在一个更大的事务中,最终控制事务。

也就是说,它并没有解决updateUser()事务失效的问题,内部其实还是this.updateUser(),是普通方法调用。之所以最终看起来好像事务控制成功,是因为updateUser()内部的异常沿着方法调用链向上抛,到了selectUser()这里触发了回滚。

讲完了方法1起效的本质后,我们再来聊聊为什么userService.selectUser()在调用时明明是代理对象:

怎么到了selectUser()内部时,this就成普通对象了呢:

请注意,即使我现在在selectUser()上加了@Transactional注解,里面的this还是普通对象。也印证了我上面的观点:方法1并没有解决updateUser()事务失效的问题,因为它还是用this普通对象调用updateUser(),并不会触发事务控制。

总而言之,此时this != userService。是不是觉得很不可思议?

Why?

这要从动态代理的底层原理说起(请参考之前动态代理相关的文章),简而言之就是下面这幅图:

动态代理的原理是,我们可以在InvocationHandler的invoke()方法中使用target目标对象调用目标方法,最终得到的效果和静态代理是一样的:

所以在add()方法里使用this,其实得到的是target,也就是目标对象,而不是代理对象。

Spring自动注入时,其实是把代理对象注入到每一个@Autowired private UserService userService中。我们在Controller调用userService代理对象的add()方法时,最终会转到目标对象的add()方法。

讲完上面方法1的原理,方法2和方法3就无需多言了吧。只不过方法3得到代理对象的方式有点奇特:

最后的最后,在讨论事务控制是否起效时,本文的一切论点都是基于以下2点:

  • 首先,要是代理对象
  • 其次,方法上要有@Transactional(或者xml配置形式)

至于为什么代理对象的方法上加了@Transactional就会触发事务,需要去看Spring的AOP源码,里面涉及到了责任链模式和递归算法。大体思路是:

0.在Spring AOP的世界里,一个个增强方法(增强代码)会被包装成一个个拦截器,放在拦截器链中。

1.代理对象调用每个方法时,其实最终都会被导向一个叫CglibAopProxy.intercept()的方法,而这个方法会判断当前方法有没有需要执行的拦截器链chain。

简单来说就是:

// 获取拦截器链if(chain.isEmpty() && Modifier.isPublic(method.getModifiers())){// 执行目标方法
} else {// 走拦截器链...
}

点进去else分支的代码,会看到:

“方法为public”时才会返回methodProxy,也能被代理。也验证了@Transactional失效的另一个情况:方法不为public时,@Transactional失效。

2.当public方法加了@Transactional,事务控制的代码就会被加入到拦截器链中,最终就会出现在事务方法的前后调用。

特别要注意,任何Java代码层面的事务控制其实还是依赖于setAutoCommit(false),也就是先关闭默认提交,此时MySQL底层就会通过日志把一连串操作先记录起来,最后一起提交。如果中间失败了,仍可根据日志回滚。具体实现细节可以去查阅MySQL事务相关资料。

另外大家可以关注下上面invokeWithinTransaction()的第二行代码,里面有一句

tas.getTransactionAttribute(method, targetClass)

本质就是传入当前事务方法和Class对象,读取上面@Transactional的注解属性,比如我们对rollbackFor和propagation的设置。

然后再往下会调用

TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

传入一些参数判断决定是否真的开启事务(名字很形象,createTransactionIfNecessary),如果我们没有使用@Transactional,就不会开启事务了。

重新理解rollbackFor和propagation

相信大家以前也看了很多类似的文章,但是看完就忘了。既然花了时间,肯定还是希望能一劳永逸。所以本文也不打算这么蜻蜓点水般结束,而是来个回马枪,和大家一起重新看看这两个属性,相信理解会更深刻。

先说结论:

  • 并不是所有的异常都会触发事务回滚,所以最好指定rollbackFor(一般图省事都直接指定Exception.class)
  • propagation是写给调用者看的,而不是写给被调用者看的(一句话解释有点晦涩,后面展开)

最好指定rollbackFor

我们来看看rollbackFor的注释:

也即是说,虽然rollbackFor默认指定了异常类型,但仅仅包括Error和RuntimeException。如果是其他自定义的业务异常,就不会触发回滚(理论上是这样,但通常业务异常都会继承自RuntimeException,因为运行时异常无需强制处理)。

propagation的案例

接下来结合上面的selectUser(),我们来看看propagation每种情况的具体演示。

Propagation.REQUIRED

如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务

selectUser()和updateUser()都加上事务控制时,虽然内部调用还是this.updateUser(),是普通方法调用,但整体上在selectUser()的事务中。

Propagation.SUPPORTS

如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。

事务失效了。

原因是test方法调用userService.selectUser()时,本身是没有事务的,而刚好selectUser()使用了SUPPORT:当前存在事务,则加入事务;如果不存在事务,则以非事务方式继续运行。

这里所谓的当前,其实就是指调用方,即调用selectUser()的方法是否存在事务。由于test不存在事务,于是selectUser()也就没有事务,而this.updateUser()本身事务失效,所以最终整个调用事务失效。

如果希望selectUser()事务起效,SUPPORTS的情况下,可以给调用方加@Transactional:

Propagation.MANDATORY

mandatory:强制的。

如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。也就是要求调用方必须存在事务。

同理,给test方法加上事务,那么selectUser()就会处于test的事务中,不会抛异常。

看到这里,大家是不是同意本小节开头说的那句话了呢:

propagation是写给调用者(test)看的,而不是写给被调用者(updateUser)看的

Propagation.REQUIRES_NEW

重新创建一个新的事务,和外面的事务相互独立。

比如:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
methodA(){// 1.插入a表...// 2.调用methodBmethodB();// 3.在methodA抛异常,回滚int i = 1/0;
}@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
methodB(){// 4.插入b表
}

methodA抛异常了,回滚了,但是methodB还是会插入记录。因为methodB是REQUIRES_NEW,自己起了一个事务。也就是说,methodA和methodB各管各的,无论是谁的内部抛异常都不会影响外部回滚。

Propagation.NOT_SUPPORTED

以非事务的方式运行,无论调用者是否存在事务,自己都不受其影响。和Propagation.REQUIRES_NEW有点像,但NOT_SUPPORTED自己是没有事务的。

Propagation.NEVER

以非事务的方式运行,如果当前存在事务,则抛出异常。即如果methodB设置了NEVER,而methodA设置了事务,那么调用methodB时就会抛异常。它不想在有事务的方法内运行。

Propagation.NESTED

和Propagation.REQUIRED效果一样。

最后说一句,我平时就看过第一、第二种。99%情况下都是默认REQUIRED,只需注意rollbackFor即可。

本文讨论是同类内的非事务方法调用事务方法,而不是调用其他类的事务方法,那和代理对象调用没区别。

@Service
class UserServiceImpl implements UserService {@Autowiredprivate StudentService studentService;public void methodA(){// 方法内部的一些操作...// 调用同类的methodB()methodB();// 调用StudentService的方法studentService.methodC();     }@Transactional(rollbackFor = Exception.class)public void methodB(){}
}

另外,大家以前可能在各种平台看过@Async注解也存在同类方法调用失效的问题。看完这篇文章,你觉得是为什么呢~

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/215673.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解读 | 为什么有很多名人让人们警惕人工智能

大家好&#xff0c;我是极智视界&#xff0c;欢迎关注我的公众号&#xff0c;获取我的更多前沿科技分享 邀您加入我的知识星球「极智视界」&#xff0c;星球内有超多好玩的项目实战源码和资源下载&#xff0c;链接&#xff1a;https://t.zsxq.com/0aiNxERDq 这个话题总能引起很…

六、ZGC深度剖析

一、引言 对于Java 程序员来说&#xff0c;JVM 帮助我们做了很多事情。 JVM是虚拟机&#xff0c;能够识别字节码&#xff0c;就是class文件或者你打包的jar文件&#xff0c;运行在操作系统上。 JVM帮我们实现了跨平台&#xff0c;你只需要编译一次&#xff0c;就可以在不同的…

在线课堂知识付费小程序源码系统 开发组合PHP+MySQL:用手机随时随地地学习,讲师亲自在线授业解惑 带安装部署教程

近年来&#xff0c;人们对于学习的需求也日益增加。传统的课堂教学已经无法满足人们的学习需求&#xff0c;而在线课堂则能够让人们随时随地地进行学习。同时&#xff0c;随着知识付费的兴起&#xff0c;越来越多的讲师也愿意将自己的知识和经验分享给更多的人。因此&#xff0…

如何管理医疗设备用电?这才是最佳方法!

随着社会对可持续发展和环保的关注不断上升&#xff0c;蓄电池监控系统作为能源存储和管理的关键技术&#xff0c;正在崭露头角。 蓄电池监控系统不仅为能源行业带来了新的可能性&#xff0c;同时也为各个领域的能源使用者提供了更加智能、高效的解决方案。 客户案例 工业生产…

ansible部署安装Tomcat

我们需要用到的文件jdk以及tomcat安装包 下载链接:https://pan.baidu.com/s/1sjG8Yl8k-SUbOv7KwKXZMA 提取码&#xff1a;t71z 准备n台机器&#xff08;我这里就简单部署三台机器&#xff09; ansible的安装部署以及配置可以看博主之前的文章自动化运维工具-ansible部署 ansib…

建筑可视化数据大屏汇总,UI源文件(PC端大屏设计)

酷炫的大屏设计让数据更好的展现&#xff0c;方便业务人员分析数据&#xff0c;辅助领导决策。现在分享大屏Photoshop源文件&#xff0c;以下为部分截图示意。 划重点&#xff1a;文末可获得完整素材包~ 01 科技建筑平台数据可视化 02 建筑公司可视化数据汇总平台 03 深蓝…

JVM虚拟机系统性学习-对象存活判断算法、对象引用类型和垃圾清除算法

垃圾回收 在 JVM 中需要对没有被引用的对象&#xff0c;也就是垃圾对象进行垃圾回收 对象存活判断算法 判断对象存活有两种方式&#xff1a;引用计数法、可达性分析算法 引用计数法 引用计数法通过记录每个对象被引用的次数&#xff0c;例如对象 A 被引用 1 次&#xff0c…

多示例VS多标签VS多示例多标签-week2

一、多示例 多示例学习属于弱监督学习中的一种&#xff0c;在对模型进行训练时&#xff0c;我们需要把训练数据分成正负包&#xff0c;再将每个包分成大小相同的示例&#xff0c;并且我们只对包的正负进行标注&#xff0c;而不对示例进行分类。当某个包被标识为正时&#xff0c…

Python常见面试知识总结(二):数据结构、类方法及异常处理

【十三】Python中assert的作用&#xff1f; Python中assert&#xff08;断言&#xff09;用于判断一个表达式&#xff0c;在表达式条件为 f a l s e false false的时候触发异常。 断言可以在条件不满足程序运行的情况下直接返回错误&#xff0c;而不必等待程序运行后出现崩溃…

【项目管理】如何用思维导图做计划?

思维导图是一种可视化的思维工具&#xff0c;它可以让我们的思考过程变得很直观。它可以帮助我们考虑到计划的各个方方面面&#xff0c;确定各要素之间的关系。 思维导图总结功能很强&#xff0c;完成计划后&#xff0c;可以用思维导图进行总结&#xff0c;为下一次做计划积累…

使用【ShardingSphere】分库分表

前言 ShardingSphere可以支撑分库分表&#xff0c;刚果商城采用了垂直分库&#xff08;根据不同业务拆分数据库&#xff09;&#xff0c;因此此文章只演示水平分表。 垂直分库 不同业务拆分为不同的数据库&#xff08;例如商城业务&#xff09; 水平分表 分表可以通过将大表拆…

移液器吸头材质选择——PFA吸头在半导体化工行业的应用

PFA吸头是一种高性能移液器配件&#xff0c;这种材料具有优异的耐化学品、耐热和电绝缘性能&#xff0c;使得PFA吸头在应用中表现出色。那么它有哪些特点呢&#xff1f; 首先&#xff0c;PFA吸头具有卓越的耐化学腐蚀性能。无论是酸性溶液、碱性溶液还是有机溶剂&#xff0c;P…

如何用CHAT帮你提高工作效率?

问CHAT&#xff1a;从规范项目管理流程交付&#xff0c;分别对项目信息安全管理&#xff0c;项目预算管理和项目采购管理三个方面提建议 CHAT回复&#xff1a; 项目信息安全管理: 1. 制定详细的信息安全政策&#xff0c;所有参与项目的员工必须遵守&#xff0c;对其中涉及敏感…

wpf TelerikUI使用DragDropManager

首先&#xff0c;我先创建事务对象ApplicationInfo&#xff0c;当暴露出一对属性当例子集合对于构成ListBoxes。这个类在例子中显示如下代码&#xff1a; public class ApplicationInfo { public Double Price { get; set; } public String IconPath { get; set; } public …

亚马逊S3V4验签与MINIO验签区别

1、先看下官方文档 AWS S3V4 DEMO 2、实际调用试试 1&#xff09;代码 // 计算auth// for a simple GET, we have no body so supply the precomputed empty hashMap<String, String> headers new HashMap<String, String>();headers.put("x-amz-content…

0013Java安卓程序设计-ssm酒品移动电商平台app

文章目录 **摘要**目录系统实现5.1 APP端5.2管理员功能模块开发环境 编程技术交流、源码分享、模板分享、网课分享 企鹅&#x1f427;裙&#xff1a;776871563 摘要 首先,论文一开始便是清楚的论述了系统的研究内容。其次,剖析系统需求分析,弄明白“做什么”,分析包括业务分析…

Firewalld 防火墙配置

文章目录 Firewalld 防火墙配置1. Firewalld 概述2. 区域名称及策略规则3. Firewalld 配置方法4. Firewalld 参数和命令5. Firewalld 两种模式6. Firewalld 使用 Firewalld 防火墙配置 1. Firewalld 概述 firewalld 是一个动态防火墙管理器&#xff0c;作为 Systemd 管理的防…

【docker】常用命令

启动docker服务 systemctl start docker 停止docker服务 systemctl stop docker 重启docker服务 systemctl restart docker 查看docker服务状态 systemctl status docker 设置开机启动docker服务 systemctl enable docker 设置关闭开机启动docker服务 systemctl disable …

数据在内存中的存储(浮点型篇)

1.例子&#xff1a;5.5&#xff1a;内存存储为101.1&#xff0c;十分位百分位依次为2的-1次方&#xff0c;2的-2次方&#xff0c;而使用科学计数法可以改写为1.011*2的2次方 2.国际标准公式&#xff1a;-1的D次方*M*2的E次方&#xff0c;x1负0正 3.M在存储时默认整数部分为1&…