@Transactional失效问题

news2024/11/15 10:32:45
作者简介:大家好,我是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

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

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

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

相关文章

应用在LED灯光控制触摸屏中的触摸芯片

LED灯光控制触摸屏方法&#xff0c;包括&#xff1a;建立触摸屏的触摸轨迹信息与LED灯光驱动程序的映射关系&#xff1b;检测用户施加在触摸屏上的触摸轨迹&#xff0c;生成触摸轨迹信息&#xff1b;根据生成的触摸轨迹信息&#xff0c;调用对应的LED灯光驱动程序&#xff0c;控…

算法-05-二分查找

二分查找&#xff08;Binary Search&#xff09;算法&#xff0c;也叫折半查找算法&#xff0c;是一种针对有序数据集合的查找算法。 1-二分查找的思想 我们生活中猜数字的游戏&#xff0c;告诉你一个数据范围&#xff0c;比如0-100&#xff0c;然后你说出一个数字&#xff0c…

​pathlib --- 面向对象的文件系统路径​

3.4 新版功能. 源代码 Lib/pathlib.py 该模块提供表示文件系统路径的类&#xff0c;其语义适用于不同的操作系统。路径类被分为提供纯计算操作而没有 I/O 的 纯路径&#xff0c;以及从纯路径继承而来但提供 I/O 操作的 具体路径。 如果以前从未用过此模块&#xff0c;或不确定…

1、springboot项目运行报错

问题1&#xff1a;获取不到配置文件的参数 我的配置文件获取的参数如下&#xff1a; public class Configures{Value("${configmdm.apk.apkName}")private static String apkName;private void setApkName(String apkName) {Configures.apkName apkName;}private …

k8s详细教程(一)

—————————————————————————————————————————————— 博主介绍&#xff1a;Java领域优质创作者,博客之星城市赛道TOP20、专注于前端流行技术框架、Java后端技术领域、项目实战运维以及GIS地理信息领域。 &#x1f345;文末获取源码…

OpenSSL 编程指南

目录 前言初始化SSL库创建SSL 上下文接口(SSL_CTX)安装证书和私钥加载证书(客户端/服务端证书)加载私钥/公钥加载CA证书设置对端证书验证例1 SSL服务端安装证书例2 客户端安装证书创建和安装SSL结构建立TCP/IP连接客户端创建socket服务端创建连接创建SSL结构中的BIOSSL握手服务…

数据结构基础介绍

一.起源及重要性 1968 年&#xff0c;美国的高德纳 Donakl E . Kn uth 教授在其所写的《 计算机程序艺术》第一卷《基本算法 》 中&#xff0c;较系统地阐述了数据的逻辑结构和存储结构及其操作&#xff0c; 开创了数据结构的课程体系 &#xff0c;数据结构作为一门独立的…

基于单片机的定时插座在智能家居中的应用

近年来&#xff0c;随着科学技术的发展迅速&#xff0c;人们对智能化的要求越来越高。越来越多的智能化产品进入千家万户&#xff0c;如电脑电视、扫地机器人、智能空气净化器等。这些家居电器和电子产品大都需要连接电源&#xff0c;为满足多种用电器的正常使用&#xff0c;延…

LeetCode力扣每日一题(Java):58、最后一个单词的长度

一、题目 二、解题思路 1、我的思路 先将字符串转换成字符数组 由于我们需要获取最后一个单词的长度&#xff0c;所以我们从后往前遍历字符数组 我们还需判断所遍历的字符是不是字母&#xff0c;即判断每个字符对应的ASCII值即可&#xff0c;用计数器count来储存单词长度 …

sudo -i 和 sudo -s

一、sudo xxx 以root权限执行单条命令 二、sudo -i 进入一个持续的root环境&#xff0c;以root权限执行命令&#xff0c;但并不是切换到root用户 三、sudo -s 也是进入一个持续的root环境&#xff0c;以root权限执行命令&#xff0c;和sudo -i的区别是保存了原来普通用…

记录 DevEco 开发 HarmonyOS 应用开发问题记录 【持续更新】

HarmonyOS 应用开发问题记录 HarmonyOS 应用开发问题记录一、预览器无法成功运行?如何定位预览器无法编译问题? 开发遇到的问题 HarmonyOS 应用开发问题记录 一、预览器无法成功运行? 大家看到这个是不是很头疼? 网上能看到许多方案,基本都是关闭一个配置 但是他们并…

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

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

【QT入门】基础知识

一.认识Qt qt是一套应用程序开发库&#xff0c;与MFC不同是跨平台的开发类库&#xff0c;主要用来开发图形界面。完全面向对象容易扩展。 优点&#xff1a;1.封装性强&#xff0c;简单易学 2.跨平台 3.独立编译为本地代码 二.qt工程 1.常见的工程文件有这两种…

PDF文件的限制编辑,如何设置?

想要给PDF文件设置一个密码防止他人对文件进行编辑&#xff0c;那么我们可以对PDF文件设置限制编辑&#xff0c;设置方法很简单&#xff0c;我们在PDF编辑器中点击文件 – 属性 – 安全&#xff0c;在权限下拉框中选中【密码保护】 然后在密码保护界面中&#xff0c;我们勾选【…

DevEco Studio 生成HPK文件

DevEco Studio 生成HPK文件 一、安装环境 操作系统: Windows 10 专业版 IDE:DevEco Studio 3.1 SDK:HarmonyOS 3.1 二、生成HPK文件 生成的HPK文件存放在entry文件夹下。下图是未生成HPK的样式。 生成HPK&#xff1a;菜单Build->Build Hap(s)/APP(s)->Build Hap(s)…

JavaFx实现图片轮播(二)

上一篇轮播文章发布后&#xff0c;很多人私信我能不能像网页效果一样显示轮播图呢&#xff1f; 那么本篇文章就给大家上实现代码&#xff0c;好了废话不多说&#xff0c;代码如下&#xff1a; fxml文件&#xff1a; <?xml version"1.0" encoding"UTF-8&qu…

MySQL InnoDB Replication部署方案与实践

1. 概述 MySQL Innodb ReplicaSet 是 MySQL 团队在 2020 年推出的一款产品&#xff0c;用来帮助用户快速部署和管理主从复制&#xff0c;在数据库层仍然使用的是主从复制技术。 ReplicaSet 主要包含三个组件&#xff1a;MySQL Router、MySQL Server 以及 MySQL Shell 高级客户…

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

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

基于SpringBoot的就业信息管理系统设计与实现(源码+数据库+文档)

摘 要 在新冠肺炎疫情的影响下&#xff0c;大学生的就业问题已经变成了一个引起人们普遍重视的社会焦点问题。在这次疫情的冲击之下&#xff0c;大学生的就业市场的供求双方都受到了不同程度的影响&#xff0c;大学生的就业情况并不十分乐观。目前&#xff0c;各种招聘平台上…

C语言经典错误总结(一)

注&#xff1a;本文是结合《C陷阱和缺陷》所写&#xff01; 一.和 我们都知道在C语言中表示赋值操作符&#xff0c;表示比较&#xff0c;那么你知道为啥单等号为&#xff0c;双等号为比较吗&#xff1f; 这里扩展下&#xff1a;因为在C语言中赋值操作符相对于比较符号较常出…