《后端程序猿 · Spring事务失效场景》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗

🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

文章目录

    • 写在前面的话
    • Spring 事务失效场景
      • 方法访问修饰符非public
      • 方法使用 static
      • 方法使用 final
      • 同类方法自调用(★)
      • 异步调用场景(★)
      • 没有被 Spring 管理
      • 异常类型不符合
      • 传播行为不当
      • 其他场景
    • 扩展 · 部分源码分析
    • 总结陈词

写在前面的话

Spring 事务管理是通过 AOP(面向切面编程)实现的,提供了声明式事务管理的能力。尽管 Spring 提供了强大的事务管理功能,但在某些情况下,事务可能会失效。

推荐文章

《故障复盘 · 记一次事务用法错误导致的大量锁表问题》


Spring 事务失效场景

方法访问修饰符非public

场景描述:

Java 的访问权限主要是:private、default、protected、public,它们的权限则是依次变大。

如果事务方法的访问修饰符是 protected、private 或 default,Spring AOP 代理无法拦截这些方法。

逻辑分析:

AbstractFallbackTransactionAttributeSource类 的 computeTransactionAttribute 方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {// Don't allow non-public methods, as configured.if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {return null;}//省略部分代码
}

解决方案:

将事务方法的访问修饰符设置为 public。


方法使用 static

场景描述:

如果事务方法被声明为 static,则 Spring AOP 代理无法拦截该方法。

逻辑分析:

静态方法属于类本身,而不是类的实例。Spring AOP 代理是基于实例的,因此无法对静态方法进行代理。

解决方案:

避免将事务方法声明为 static。


方法使用 final

场景描述:

在 Java 中,final 关键字用于修饰类、方法和变量,表示这些元素不能被修改或重写。

如果事务方法被声明为 final,则 Spring AOP 代理无法拦截该方法。

逻辑分析:

final 方法在编译时会被优化,无法被子类重写。CGLIB 代理是通过子类化来实现的,因此无法代理 final 方法。

例如,CGLIB 代理的实现中,如果方法被标记为 final,则在生成代理类时会跳过这些方法:

public class Enhancer {protected void generateMethod(ClassGenerator gen, Method method) {// 跳过 final 方法if (Modifier.isFinal(method.getModifiers())) {return; }}
}

解决方案:

避免将事务方法声明为 final。

Tips:上面几个错误很明显,IDEA也会给出相应提示,应该不容易会出现。


同类方法自调用(★)

场景描述:

当一个类中的方法 A 调用同一类中的方法 B 时,如果方法 B 上有事务注解(如 @Transactional),而方法 A 没有,事务可能会失效。

逻辑分析:

如事务注解 @Transactional 是基于动态代理实现的,Spring 采用动态代理(AOP)实现对 bean 的管理和切片,它为我们的每个 class 生成一个代理对象。只有在代理对象之间进行调用时,可以触发切面逻辑。而在同一个 class 中,方法B调用方法A,调用的是原对象的方法,而不通过代理对象,所以 Spring 无法切到这次调用,也就无法通过注解保证事务性了。

友情提示:@Aspectj、@Async,@Transational、@Cacheable 等注解都是基于AOP 实现的,AOP是基于动态代理实现,存在问题差不多。

原理补充:

由于 Spring AOP 采用了动态代理实现,在Spring 容器中的bean(也就是目标对象)会被代理对象代替,代理对象里加入了我们需要的增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截。通过调用代理对象的A方法,在其内部会经过切面增强,然后方法被发射到目标对象,在目标对象上执行原有逻辑,如果在原有逻辑中(同类)嵌套调用了B方法,则此时B方法并没有被进行切面增强,因为此时它已经在目标对象内部。而解决方案很好地说明了,将嵌套方法发射到代理对象,这样就完成了切面增强。

解决方案1:

迁移方法,把业务逻辑抽离到另外一个Service,然后正常注入,调用。

这种方式最稳妥,但是改造量可能偏大。

解决方案2:

获取本对象的代理对象,再进行调用,有两种方式:

1、从Bean工厂获取,注入自身,或者通过getBean方式;

2、使用 AopContext.currentProxy() 方式;

Tips:注入自身的问题,由于这种写法基于Spring的三级缓存不会导致循环依赖的问题出现。

解决方案3:

使用编程式事务,自主控制事务的提交和回滚。


异步调用场景(★)

场景描述:

如果你将 @Async 和 @Transactional 注解放在同一个方法上,通常会导致事务失效。这是因为 @Async 注解会导致该方法在一个新的线程中执行,而 Spring 的事务管理是基于代理的,通常只在同一线程中有效。

逻辑分析:

1、代理机制:Spring 的事务管理是通过 AOP(面向切面编程)实现的,通常使用 JDK 动态代理或 CGLIB 代理。当你在一个方法上使用 @Transactional 注解时,Spring 会创建一个代理对象来管理事务。

2、异步执行:当你在同一个方法上使用 @Async 注解时,Spring 会将该方法的调用转发到一个新的线程中执行。由于事务是绑定到调用线程的,而不是代理对象的,因此在新的线程中,事务不会生效。

解决方案1:

将事务和异步调用分开:将 @Transactional 注解放在一个单独的方法上,然后在该方法中调用带有 @Async 注解的方法。

解决方案2:

如果一定要先用异步逻辑,那么可以在异步逻辑中使用编程式事务。

补充:为什么同时加 @Async 和 @Transactional 时,异步生效而不是事务生效?

这主要是因为 Spring 的事务管理机制 和 异步执行机制 的工作原理不同。

1、事务管理机制:Spring 的事务管理是基于 代理 的。当一个方法被 @Transactional 注解标记时,Spring 会为该方法生成一个代理对象。这个代理对象会拦截方法调用,并在方法执行前后进行事务管理操作(例如,开始事务、提交事务、回滚事务)。

2、异步执行机制:Spring 的异步执行机制是基于 线程池 的。当一个方法被 @Async 注解标记时,Spring 会将该方法提交到一个线程池中执行。线程池会创建一个新的线程来执行该方法,而这个新的线程与当前线程是独立的。

当 @Async 和 @Transactional 同时存在时,Spring 会优先处理 @Async 注解。这意味着方法会被提交到线程池中执行,而不是被代理对象拦截。由于事务管理是基于代理的,因此在异步线程中,事务管理机制无法生效。简单来说,事务管理需要在同一个线程中进行,而异步执行会创建新的线程,导致事务管理无法生效。

总之,@Async 注解会将方法提交到线程池中执行,而 @Transactional 注解会为方法生成代理对象。Spring 会优先处理 @Async 注解,因此事务管理机制无法在异步线程中生效。


没有被 Spring 管理

场景描述:

如果一个被 @Transactional 注解的方法被一个非 Spring 管理的类调用,事务也会失效。

逻辑分析:

这个好像不用多说了,SpringAOP都不会触发。

直接检查一下是不是Bean扫描路径不对等问题。


异常类型不符合

场景描述:

默认情况下,Spring 只会对未检查异常(RuntimeException)进行回滚。如果抛出的是检查异常(Exception),事务不会自动回滚,除非在 @Transactional 注解中指定。

逻辑分析:

@Transactional
public void createUser() {// 业务逻辑throw new IOException(); // 事务不会回滚
}

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

解决方案:

按需调整,例如:@Transactional(rollbackFor = Exception.class)


传播行为不当

场景描述:

事务的传播行为决定了方法调用时如何处理事务。如果传播行为设置不当,可能导致事务失效。例如,使用 Propagation.REQUIRES_NEW 会导致新事务的创建,而不是在现有事务中执行。

逻辑分析:

这种情况不能说事务失效,只能说要按自己的需要处理。


其他场景

1、数据库不支持事务(较少);

2、项目没有开启事务能力(较少);

3、开发捕获了异常导致没回滚(常见);

4、未完待续。。。


扩展 · 部分源码分析

模拟一个事务方法调用,当该方法被调用时,Spring AOP 会拦截这个调用,进入下面的流程。

Step1、事务拦截器触发

参考:TransactionInterceptor#invoke

说明:TransactionInterceptor 会调用 invoke 方法,检查方法上是否有 @Transactional 注解。

Step2、获取事务属性

参考:AbstractFallbackTransactionAttributeSource#getTransactionAttribute

说明:getTransactionAttribute 方法来获取事务属性,这里还涉及一个缓存机制,先不管。

Step3、开启事务

参考:TransactionAspectSupport#invokeWithinTransaction

说明:通过 PlatformTransactionManager 的 getTransaction 方法开始一个新事务,这会创建一个 TransactionStatus 对象,表示当前事务的状态。

Step4、执行目标方法

参考:retVal = invocation.proceedWithInvocation()

说明:执行目标方法,操作数据库。

Step5、提交或回滚事务

如果目标方法执行成功,TransactionInterceptor 会调用 commitTransactionAfterReturning/commit 方法提交事务。

如果在执行过程中抛出异常,TransactionInterceptor 会调用 completeTransactionAfterThrowing/rollback 方法回滚事务。

Step6、结束事务

参考:cleanupTransactionInfo

说明:TransactionInterceptor 会清理事务状态,结束事务。


总结陈词

此篇文章介绍了 Spring 事务的常见失效场景,仅供学习参考。

通过本篇文章的分析,可以看到,Spring事务失效的原因,大半部分和SpringAOP原理有关系,如果某些因素导致AOP无法生效或代理类无法操作,则事务随之失效了。从源码分析过程中,也能找到部分事务失效场景对应的代码。

Spring 的事务能讨论的知识点还很多,后续有机会再进行专题补充,

💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

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

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

相关文章

git commit -am 仅提交已修改文件

git commit -am 是一个 Git 命令&#xff0c;用于将所有已跟踪&#xff08;tracked&#xff09;文件的修改添加到暂存区&#xff0c;并同时创建一个新的提交。 具体解释&#xff1a; -a&#xff1a;表示自动暂存所有已跟踪的文件的修改&#xff08;包括修改和删除&#xff09…

2024/9/29周报

文章目录 摘要Abstract污水处理工艺流程整体介绍粗格栅细格栅曝气沉砂池提升泵房峰谷平策略 初沉池&#xff08;一级处理&#xff09;工作原理运行管理 氧化沟生化池&#xff08;二级处理&#xff09;二沉池工作原理运行参数 高效沉淀池功能与特点工作原理 深度处理&#xff08…

LeetCode 918. 环形子数组的最大和

原题链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 给定一个长度为 n 的环形整数数组 nums &#xff0c;返回 nums 的非空 子数组 的最大可能和 。 环形数组 意味着数组的末端将会与开头相连呈环状。形式上&#xff0c; nums[i] 的下一个元素是 nums[(i 1) % n…

防止错误输入!Excel单元格限制输入内容的三种有效方式

在Excel中&#xff0c;限制单元格输入内容可以帮助避免数据输入错误&#xff0c;确保数据的一致性和准确性。今天小编分享三种方法&#xff0c;可以轻松限制Excel单元格的输入内容&#xff0c;确保数据输入符合预期要求&#xff0c;一起来看看吧&#xff01; 方法一&#xff1a…

Register Two Point Sets 注册两个点集

文章目录 Register Two Point Sets 注册两个点集Visualize Gradient Descent 可视化梯度下降Hyperparameter Search 超参数搜索JensenHavrdaCharvatTsallisPointSetToPointSetMetricv4类说明 原文url: https://examples.itk.org/src/registration/metricsv4/registertwopointse…

【AI驱动TDSQL-C Serverless数据库技术实战】 AI电商数据分析系统——探索Text2SQL下AI驱动代码进行实际业务

目录 一、Text2SQL简介二、基于TDSQL-C Serverless的Text2SQL实战2.1、程序流程图2.2、实践流程2.2.1、配置TDSQL-C2.2.2、部署LLAMA模型2.2.3、本地依赖安装2.2.4、应用构建 2.3、运行效果 三、Text2SQL下的AI驱动 Text2SQL 是一种将自然语言查询转换为 SQL 查询的技术&#x…

中航资本:沪市主板代码以几开头?各板块开头代码是多少

各板块开始代码&#xff1a; 场内商场&#xff1a; 1、沪市主板&#xff1a;股票代码以600、601、603、605开始。 60开始的股票都是在上海证券交易所上市的股票。 600开始是上海证券交易所上市的一般股票&#xff0c;601开始的股票是主板股票&#xff0c;一般是大盘股蓝筹股…

Hi.Events —— 您的全方位活动管理与票务平台

大家好&#xff01;今天给大家介绍一个超厉害的开源项目&#xff1a;Hi.Events&#xff0c;这是一个功能丰富的自托管活动管理和票务平台&#xff0c;无论是会议还是俱乐部活动&#xff0c;它都能帮你轻松搞定&#xff01; 项目介绍 Hi.Events是一款功能丰富、自托管的开源活动…

Service和Endpoints

在 Kubernetes 中&#xff0c;Service 和 Endpoints 是两个非常重要的资源对象&#xff0c;它们共同用于定义和管理集群内部的服务发现和网络通信。下面详细介绍这两个资源对象的功能及其相互关系。 Service Service 是 Kubernetes 中用于定义抽象逻辑服务的资源对象。它提供…

学习Webpack中图片-JS-Vue-plugin

目录 图片文件资源模块类型 JS文件babel命令行使用babel-loaderbabel-preset Vue文件vue-loadervue/compiler-sfc pluginCleanWebpackPluginHtmlWebpackPluginDefinePlugin 图片文件 需要先在项目中使用图片&#xff0c;比较常见的使用图片的方式是两种&#xff1a; img元素&…

React Native中如何调用iOS的Face ID和Android的生物识别,react-native-biometrics

在React Native中调用Android和iOS的Face ID&#xff08;iOS特有&#xff09;或类似的功能&#xff08;如Android上的生物识别&#xff0c;通常是通过指纹或面部识别&#xff09;&#xff0c;你需要分别处理两个平台&#xff0c;因为这两个操作系统提供的API和框架不同。 对于…

Linux【基础指令汇总】

目录 Linux命令的特点 1、文件管理 ls命令 cp命令 mkdir命令 mv命令 pwd命令 2、文档编辑 cat命令 echo命令 rm命令 tail命令 rmdir命令 3、系统管理 rpm命令 find命令 startx命令 uname命令 vmstat命令 4、磁盘管理 df命令 fdisk命令 lsblk命令 hdpar…

C语言_回调函数和qsort

1. 回调函数 回调函数就是一个通过函数指针调用的函数。 通俗易懂些讲就是把函数的指针作为参数传递给另一个函数&#xff0c;当在另一个函数中通过这个指针调用其所指向的函数时&#xff0c;那这个通过指针被调用的函数就叫做回调函数。 先上一个模拟计算机的代码&#xff…

Docker安装mysql8并配置主从复制

1. 安装mysql8 1.1 新增挂载文件 # 新增mysql挂载文件夹 mkdir -p /root/docker/mysql/m01/log mkdir -p /root/docker/mysql/m01/data mkdir -p /root/docker/mysql/m01/conf1.2 新增mysql配置文件 # 新增mysql配置文件 cd /root/docker/mysql/m01/conf vim my.cnf # 下面是…

关于git分支冲突问题

什么是冲突 在Git中&#xff0c;冲突是指两个或多个开发者对同一文件统一部份进行了不同的修改&#xff0c;并且在合并这些修改时&#xff0c;Git无法自动确定应该采用哪种修改而产生的情况。 分支冲突 如何出现并解决 在一个版本时&#xff0c;有一个master分支&#xff0c…

如何使用WinRAR锁定压缩文件,防止文件被修改或删除?

在日常工作中&#xff0c;我们经常需要分享压缩文件&#xff0c;但也可能面临文件被修改或删除的风险。想要保护压缩文件的完整性&#xff0c;不妨使用WinRAR提供的“锁定压缩文件”功能。这个功能可以防止文件被意外更改或删除&#xff0c;确保压缩文件保持原样。下面一起来看…

Linux中的 `vi` 与 `vim` 使用详解

文章目录 Linux中的 vi 与 vim 使用详解1. vi 编辑器1.1 什么是 vi1.2 vi 的基本用法1.2.1 启动 vi1.2.2 模式1.2.3 基本操作1.2.4 常用命令 1.3 vi 的特点 2. vim 编辑器2.1 什么是 vim2.2 vim 的基本用法2.2.1 启动 vim2.2.2 模式2.2.3 vim 的增强功能2.2.4 vim 的基本操作 2…

如何选择合适的量化交易策略,回测与模拟交易的实战演练

炒股自动化&#xff1a;申请官方API接口&#xff0c;散户也可以 python炒股自动化&#xff08;0&#xff09;&#xff0c;申请券商API接口 python炒股自动化&#xff08;1&#xff09;&#xff0c;量化交易接口区别 Python炒股自动化&#xff08;2&#xff09;&#xff1a;获取…

解决跨域问题的案列

JSONP&#xff08;JSON with Padding&#xff09; JSONP 是一种通过 <script> 标签的跨域请求方法&#xff0c;它依赖于服务器支持并返回特定格式的响应。 示例&#xff1a; 前端&#xff1a; <script> function jsonpCallback(data) {console.log(Received data:…

websocket初识

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单&#xff0c;允许服务端主动向客户端推送数据。在 WebSocket API 中&#xff0c;浏览器和服务器只需要完成一次握手&#xff0c;两者之间就直接可以创建持久性…