@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 {
	@Autowired
    private 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.调用methodB
    methodB();
    // 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 {
    @Autowired
    private StudentService studentService;
    
    public void methodA(){
        // 方法内部的一些操作
        ...
            
        // 调用同类的methodB()
        methodB();
        
        // 调用StudentService的方法
        studentService.methodC();     
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void methodB(){
        
    }
}

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

酷开科技多维度赋能营销,实力斩获三项大奖

在数智化新阶段、广告新生态、传播新业态的背景下&#xff0c;“第30届中国国际广告节广告主盛典暨网易传媒态度营销峰会”于11月18日在厦门国际会展中心盛大举行。来自全国的品牌方、战略决策者、媒体平台和品牌服务机构等汇聚一堂。在50000&#xff0b;现场观众和数千万线上观…

SSL证书HTTPS保护服务

SSL证书属于数字证书的其中一种&#xff0c;广泛用于https协议&#xff0c;从而可以让数据传输在加密前提下完成&#xff0c;确保HTTPS网络安全是申请SSL证书必要工作。 SSL证书是主要用于https是一种加密协议&#xff0c;仔细观察网站地址会发现目前主流的网址前面都会有http…

Linux操作系统学习(零)、计算机概论

计算机概论 指令集 CPU中含有多种指令集&#xff0c;指令集对于CPU运算具有指导和优化的硬程序&#xff0c;用来引导CPU进行加减运算和控制计算机操作系统的一系列指令的集合 常见的就有微指令集RISC和复杂指令集CISC RISC&#xff1a;包括ARM架构和PPC架构 CISC&#xff…

HTML面试题---专题四

文章目录 一、前言二、如何在 HTML 中嵌入音频文件&#xff1f;三、解释 <script> 标签中 defer 属性的用途。四、如何在 HTML 中创建粘性/固定导航栏&#xff1f;五、HTML 中的 span 元素的用途是什么&#xff1f;六、如何使 HTML 元素可拖动&#xff1f;七、解释 <i…

项目中使用Arrays.asList、ArrayList.subList的坑

使用Arrays.asList的注意事项 1.1 可能会踩的坑 先来看下Arrays.asList的使用&#xff1a; List<Integer> statusList Arrays.asList(1, 2); System.out.println(statusList); System.out.println(statusList.contains(1)); System.out.println(statusList.contains(3)…

如何打造稳健高效的数据库的基础设施?数据库云提出创新方案

引言 数据库的云化、丰富业务场景下多元的数据库类型、公有云与私有云交织的IT架构&#xff0c;叠加信创影响使得企业内部的基础设施日益复杂&#xff0c;如何高效管理多元的数据库和多云异构基础设施正成为企业面临的严峻挑战。 在此背景下&#xff0c;数据库云应运而生。数…

六级翻译之印章

好像大房子挺难得 三段式 1Since ancient from now&#xff0c;seals have been a symbol of power and certerfiction of identity.seals not only practical but also is a form of art.Seal is an ancient art combining with manafutuer of crafting and desgin of…

界面控件DevExpress中文教程 - 如何用Office File API组件填充PDF表单

DevExpress Office File API是一个专为C#, VB.NET 和 ASP.NET等开发人员提供的非可视化.NET库。有了这个库&#xff0c;不用安装Microsoft Office&#xff0c;就可以完全自动处理Excel、Word等文档。开发人员使用一个非常易于操作的API就可以生成XLS, XLSx, DOC, DOCx, RTF, CS…

打工人副业变现秘籍,某多/某手变现底层引擎-Stable Diffusion替换背景

在Stable Diffusion软件中,使用ControlNet+模型实现固定物体批量替换背景 出图的流程。 一、准备好图片 1.你需要准备好一些白底图或者透明底图用于训练模型。 2.你需要准备同样角度的其他背景色底图用于ControlNet勾线 3.注意检查你的图片尺寸,是否为1:1,…

安防 音响 车载等产品中音频接口选型的高性能国产芯片分析

在人工智能兴起之后&#xff0c;安防市场就成为了其全球最大的市场&#xff0c;也是成功落地的最主要场景之一。对于安防应用而言&#xff0c;智慧摄像头、智慧交通、智慧城市等概念的不断涌现&#xff0c;对于芯片产业催生出海量需求。今天&#xff0c;我将为大家梳理GLOBALCH…

STM32 CAN多节点组网项目实操 挖坑与填坑记录

摘要 CAN线性组网项目开发过程中遇到的数据丢包问题&#xff0c;并尝试解决的记录和推测分析。 关键词 CAN串联多节点通讯、CAN10节点通讯、CAN数据丢包、STM32 CAN 背景/项目介绍 概述&#xff1a; 开发了一个多节点线性组网采集数据的项目。 系统包含1个供电和数据网关板还有…

mysql:查看一个表的索引信息

可以使用命令SHOW INDEX FROM table_name;查看一个表的索引信息&#xff0c;例如&#xff1a;

NGROK内网穿透工具-实战+源码下载

1、功能概述&#xff1f; 本案例中使用ngrok内网穿透工具&#xff0c;使用方便&#xff0c;不需要注册等麻烦的操作&#xff0c;永久使用&#xff0c;一键搞定。 我们在项目中有这样一种需求&#xff1a; 甲方&#xff1a;汤工你能不能把你们正在开发的项目或者页面发给我看…

vue脚手架创建项目

安装 npm install -g vue/cli 如果报错可以尝试使用cnpm 查看命令 vue -V 创建项目 vue create 项目名称 输入y 上下选中选项 Manually select features &#xff08;自由选择&#xff09;&#xff0c;回车 vue 版本的选择 其他按需要选择 项目创建成功&#xff0c;…

C++基础知识

目录 前言&#xff1a; 命名空间 命名空间的定义 命名空间的使用 c输入与输出 缺省参数 函数重载 引用 引用的特性 常引用 引用的使用场景 引用做参数 引用做返回值 引用与指针的区别 内联函数 内联函数的特性 前言&#xff1a; C 语言是结构化和模块化的语言&…

服务器数据恢复—raid5少盘状态下新建raid5如何恢复原raid5数据?

服务器数据恢复环境&#xff1a; 一台服务器上搭建了一组由5块硬盘组建的raid5阵列&#xff0c;服务器上层存放单位重要数据&#xff0c;无备份文件。 服务器故障&分析&#xff1a; 服务器上raid5有一块硬盘掉线&#xff0c;外聘运维人员在没有了解服务器具体情况下&#x…

【教程】制作 iOS 推送证书

​ 目录 证书类型 MAC Key Store 消息推送控制台 制作证书 创建苹果 App ID 使用appuploder制作 .p12文件 创建证书 如需向 iOS 设备推送数据&#xff0c;您首先需要在消息推送控制台上配置 iOS 推送证书。iOS 推送证书用于推送通知&#xff0c;本文将介绍消息推送服务支…

以企业架构为中心的SABOE数字化转型五环法

文章目录 01 传统企业数字化转型面临诸多挑战02 SABOE数字化转型五环法为企业转型破除迷雾 01 传统企业数字化转型面临诸多挑战 即将过去的2023年&#xff0c;chatGPT大模型、数据资产入表等事件的发生&#xff0c;标志着数字经济正在加速发展。数字经济是人类社会继农业经济、…

JS实现返利网注册系统(网页数据验证)

主代码 <!DOCTYPE HTMLPUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns"http://www.w3.org/1999/xhtml"><head><title>返利网注册</tit…

被迫搬家,宽带迁移怎么办?

广州一栋违建烂尾楼&#xff0c;13年里从未停止出租&#xff0c;年年住满人。这栋楼没有贴外墙&#xff0c;裸露的水泥表面都被雨水腐蚀&#xff0c;很多阳台没有建好&#xff0c;只是简单加装了护栏&#xff0c;存在巨大安全隐患。 为什么烂尾楼年年满人呢&#xff1f; 因为它…