springboot-事务失效以及排查过程

排查了好久,终于解决,希望这次的排查过程对大家也有帮助,废话少说,上源码

开发环境

springboot 2.3.11
jdk8
gradle6.4
HikariDataSource
ps: 本环节使用双数据源,在service层做切面拦截,切换具体的数据源

问题

在定义了具体的事务管理以后,想着不要手动切库,因为事务管理那里已经显示的注入了具体的数据源,然后结果导致的是事务失效,发生异常不回滚。代码如下

    @Override@Transactional(rollbackFor = Exception.class, transactionManager = DsConst.AGLOUD_TRANSACTION_MANAGER)public void ex() {jdbcTemplate.execute("insert into table_name1(name) values (1)");if (1 == 1) throw new RuntimeException("出异常了啊!!!!");jdbcTemplate.execute("insert into table_name2(name) values (1)");}
排查过程

第一步,先排查源码,spring拦截事务的拦截器是TransactionInterceptor类的invokeWithinTransaction方法,源码如下

@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, TransactionAspectSupport.InvocationCallback invocation) throws Throwable {TransactionAttributeSource tas = this.getTransactionAttributeSource();//1:获取事务属性配置信息:通过 TransactionAttributeSource.getTransactionAttribute解析@Trasaction注解得到事务属性配置信息TransactionAttribute txAttr = tas != null ? tas.getTransactionAttribute(method, targetClass) : null;//2:获取事务管理器TransactionManager tm = this.determineTransactionManager(txAttr);//将事务管理器tx转换为 PlatformTransactionManagerPlatformTransactionManager ptm = this.asPlatformTransactionManager(tm);String joinpointIdentification = this.methodIdentification(method, targetClass, txAttr);//createTransactionIfNecessary内部,这里就不说了,内部主要就是使用spring事务硬//编码的方式开启事务,最终会返回一个TransactionInfo对象TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);//业务方法返回值Object retVal;try {//调用aop中的下一个拦截器,最终会调用到业务目标方法,获取到目标方法的返回值retVal = invocation.proceedWithInvocation();} catch (Throwable var18) {//3:异常情况下,如何走?可能只需提交,也可能只需回滚,这个取决于事务的配置this.completeTransactionAfterThrowing(txInfo, var18);throw var18;} finally {//清理事务信息this.cleanupTransactionInfo(txInfo);}if (retVal != null && vavrPresent && TransactionAspectSupport.VavrDelegate.isVavrTry(retVal)) {TransactionStatus status = txInfo.getTransactionStatus();if (status != null && txAttr != null) {retVal = TransactionAspectSupport.VavrDelegate.evaluateTryFailure(retVal, txAttr, status);}}//4:业务方法返回之后,只需事务提交操作this.commitTransactionAfterReturning(txInfo);return retVal;}

当我跟进去时候,发现事务并不是一个新的事务了,spring判断回滚的代码如下completeTransactionAfterThrowing,进去,最后执行到这里

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {try {boolean unexpectedRollback = unexpected;try {triggerBeforeCompletion(status);if (status.hasSavepoint()) {status.rollbackToHeldSavepoint();}//判断是否是一个新的事务,如果是就回滚else if (status.isNewTransaction()) {doRollback(status);}

我很疑惑,为什么不是一个新的事务,而且我也没有整嵌套事务什么花里胡哨的东西,怎么就不是新的事务了。于是我另外起了一个项目对比debugger,好久才发,具体如下
77ca8e6de60a157292f1b0faf302cf3.png

乍一看,我是不是眼花了!!!为什么目标对象和代理对象都是代理对象呢。我xx,Cglib创建代理的时候,是可以代理类也再创建代理的吗?本着求真务实的精神,用cglib的原生Api做了测试,发生会报错,不能这样子。代码如下

package com.shiguiwu.springmybatis.spring.aop.cglib;import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;/*** @description: callbak* 1. Cglib根据父类,Callback, Filter 及一些相关信息生成key* 2. 然后根据key 生成对应的子类的二进制表现形式* 3. 使用ClassLoader装载对应的二进制,生成Class对象,并缓存* 4. 最后实例化Class对象,并缓存* @author: stone* @date: Created by 2021/5/18 19:54* @version: 1.0.0* @pakeage: com.shiguiwu.springmybatis.spring.aop.cglib*/
public class CglibCallbackObjTest {interface Service1 {void m1();}interface Service2 {void m2();}public static class Service implements Service1, Service2 {@Overridepublic void m1() {System.out.println("m1");}@Overridepublic void m2() {System.out.println("m2");}}public static void main(String[] args) {Enhancer enhancer = new Enhancer();//设置父类enhancer.setSuperclass(Service.class);//设置代理对象需要实现的接口enhancer.setInterfaces(new Class[]{Service1.class, Service2.class});//通过Callback来对被代理方法进行增强enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {long l = System.nanoTime();Object result = methodProxy.invokeSuper(o, objects);System.out.println(method.getName() + "耗时为:" + (System.nanoTime() - l));return result;});Object proxy = enhancer.create();//if (proxy instanceof Service) {//    ((Service) proxy).m1();//    ((Service) proxy).m2();//}System.out.println("父类" + proxy.getClass().getSuperclass());System.out.println(proxy.getClass());System.out.println("创建代理类实现的接口如下:");for (Class<?> cs : proxy.getClass().getInterfaces()) {System.out.println(cs);}Enhancer enhancer1 = new Enhancer();enhancer1.setSuperclass(proxy.getClass());enhancer1.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {long l = System.nanoTime();Object result = methodProxy.invokeSuper(o, objects);System.out.println(method.getName() + "耗时11为:" + (System.nanoTime() - l));return result;});Object proxy1 = enhancer1.create();System.out.println("父类" + proxy1.getClass().getSuperclass());System.out.println(proxy1.getClass());//if (proxy1 instanceof Service) {//    ((Service) proxy1).m1();//    ((Service) proxy1).m2();//}}}

运行这段代码会报错

Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.ClassFormatError-->Duplicate method name&signature in class file com/shiguiwu/springmybatis/spring/aop/cglib/CglibCallbackObjTest$Service$$EnhancerByCGLIB$$c12e4df0$$EnhancerByCGLIB$$eed200b8at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:538)at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585)at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.ap

但是,要知道,spring鬼的很,说不定他就是可以的,结果测试代码,发现,spring基于cglib创建的代理是可以,他会判断当前的目标对象是不是代理对象,如果是,则以目标对象的父类来创建代理对象,但是目标对象还是代理对象。代码org.springframework.aop.framework.CglibAopProxy#getProxy(@Nullable ClassLoader classLoader) 如下

	try {Class<?> rootClass = this.advised.getTargetClass();Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");Class<?> proxySuperClass = rootClass;//判断目标对象是不是代理对象if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {proxySuperClass = rootClass.getSuperclass();Class<?>[] additionalInterfaces = rootClass.getInterfaces();for (Class<?> additionalInterface : additionalInterfaces) {this.advised.addInterface(additionalInterface);}}

这个时候,我们需要了解springbean的生命周期了,spring是什么时候,偷梁换柱的,把目标对象变成代理对象。现有我知道的就是引入@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)注解,然后spring向容器中添加了一个AnnotationAwareAspectJAutoProxyCreator,这个类很重要,它是一个SmartInstantiationAwareBeanPostProcessor的子类,他就是springbean在初始化后调用的方法,然后判断bean 需不需要生成代理对象。主要逻辑在抽象类AbstractAutoProxyCreator#postProcessAfterInitialization

      @Overridepublic Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (this.earlyProxyReferences.remove(cacheKey) != bean) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;}

然后我把断点打到这个位置,因为类比较多,为了不干扰我们调试,使用条件debugger,具体如下
image.png

最终发现,有在创建代理对象的时候,上面的方法进了两次。然后发现他们是不同的类进来的,原来我项目代码中,创建代理用到了两个类分别是AnnotationAwareAspectJAutoProxyCreator和DefaultAdvisorAutoProxyCreator。这个两个类型具有相同的父类,也是BeanPostProcessor,也会拦截bean的创建过程。

但是我本地的项目主要AnnotationAwareAspectJAutoProxyCreator,于是为了模拟现场的环境,我手动注入了DefaultAdvisorAutoProxyCreator,最后发现本地是跟上次的情况一样,创建了嵌套代理。

@Beanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);return defaultAdvisorAutoProxyCreator;}

那是不是因为嵌套代理让事务失效呢,最后的答案是:不是。本地照样能回滚,原来我第一个打点断的时候,只关注了是不是新事务,但是spring aop是一个方法调用链,所以当旧的事务通过调用栈出来的时候,这个事务就是一个新事务,也执行了回滚操作.

心累。。。
然后我在想是不是postgres没有开启事务支持呢,或者是数据库需要手动设置事务支持。通过实验我又排除了这个可能。代码如下

   @Autowiredprivate DataSource dataSource;@Resourceprivate DataSource masterDataSource;@Testpublic void tx() {JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);//1.定义一个事务管理器PlatformTransactionManager transactionManager = new DataSourceTransactionManager(masterDataSource);boolean a = true;//清空数据jdbcTemplate.update("truncate table table_name1");System.out.println("PROPAGATION_REQUIRED start ==========================================================");//2.定义一个事务属性TransactionDefinition transactionDefinition = new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED);//3.取一个事务状态,开启事务了TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);//设置扩展点//addSynchronization("ts-1", 2);//        //addSynchronization("ts-2", 1);try {//4.执行业务jdbcTemplate.execute("insert into table_name1(name) values (1)");if (a) {throw new RuntimeException("===");}//jdbcTemplate.update("insert into book(book_name) values (?)", "成是非");jdbcTemplate.execute("insert into table_name1(name) values (100)");//此时,在执行一个方法事务//other(jdbcTemplate, transactionManager);//5.提交事务System.out.println("PROPAGATION_REQUIRED 准备commit");transactionManager.commit(transactionStatus);System.out.println("PROPAGATION_REQUIRED commit完毕");} catch (Exception e) {e.printStackTrace();//6.回滚事务:platformTransactionManager.rollbacktransactionManager.rollback(transactionStatus);}System.out.println("after==========================>:" + jdbcTemplate.queryForList("SELECT * from table_name1"));}

经过上面代码,最终发现,JdbcTemplate 的数据源和PlatformTransactionManager 管理器的数据源不是同一个,为什么不是同一个就失效呢?这里需要了解spring内部是怎么控制事务的。
这里简单说一下,源码不是我们的重点:
spring在开始事务之前,会在事务管理器中拿数据源,通过该数据源获取一个数据库连接,同时将数据库连接设置为手动提交,然后通过ThreadLocal绑定到当前线程中,绑定的格式map类型datasource->ConnectionHolder。再来就是执行业务了,jdbcTemplate中有数据源,这个时候,重点来了啊。根据jdbcTemplate的数据源,从本地线程中拿连接,此时由于数据源不一样,根本拿不到连接,spring帮我们本次数据源中再创建一个连接,用此时的连接执行sql,然后自动提交了。而我们保存线程本地的连接一直没干活,所以事务失效!

再次排查

于是呢,我在事务方法哪里又手动进行切库,这回总该数据源一致了吧。虽然说是动态数据源,但是具体返回的数据源应该由我来设置的。

image.png

但是,事与愿违,还是不行,于是我在切库的切面那里打了个断点,代码如下

然后发现,代码居然是先执行了事务拦截器,在执行的切面,炸了啊。怎么可能呢,查资料也是事务拦截器优先级很低的,但是不管你怎么设置的切面顺序,始终但是先执行的事务拦截器。也就是说,事务管理里的数据源,不管你怎么切换,始终都是默认的数据源,这样就实现不了其他库的事务控制。最后回到最开始的问题,有一个事务拦截器是代理的代理,切面是目标对象的代理,所以不管你怎么设置,代理的代理方法始终是最先执行的。

最后整个人都麻了,然后只能从为什么有两个代理生成器的人手,最后通过寻找,在jar里面,有人显示注入了DefaultAdvisorAutoProxyCreator,这个是由于项目遗留的bug,将那个源码改掉就解决了。

回顾

本问题涉及的知识点比较多,事务,代理,切面,spring bean的创建过程。本人水平有限,如有错误,请批评指正,多谢!!!88888

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

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

相关文章

Docker入门之Windows安装Docker初体验

在之前我们认识了docker的容器&#xff0c;了解了docker的相关概念&#xff1a;镜像&#xff0c;容器&#xff0c;仓库&#xff1a;面试官让你介绍一下docker&#xff0c;别再说不知道了 之后又带大家动手体验了一下docker从零开始玩转 Docker&#xff1a;一站式入门指南&#…

信息与网络安全

1.对称密码体制的优缺点 优点&#xff1a;1.加密解密处理速度快 2.保密度高&#xff1b; 缺点&#xff1a;1.对称密码算法的密钥 分发过程复杂&#xff0c;所花代价高 2.多人通信时密钥组合的数量会出现爆炸性膨胀&#xff08;所需密钥量大&#xff09; 3.通信双方必须统一密钥…

GPT、Python和OpenCV支持下的空天地遥感数据识别与计算

在科技飞速发展的时代&#xff0c;遥感数据的精准分析已经成为推动各行业智能决策的关键工具。从无人机监测农田到卫星数据支持气候研究&#xff0c;空天地遥感数据正以前所未有的方式为科研和商业带来深刻变革。然而&#xff0c;对于许多专业人士而言&#xff0c;如何高效地处…

STM32完全学习——外部中断

一、嵌套向量中断控制器 我们在这里使用标准库的方式来处理。因此只需要调用几个函数就可以了。 NVIC_InitTypeDef NVIC_InitStruct; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //中断优先级分组 分1组NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0); …

【动手做】安装Miniconda和jupyter notebook环境实现线性回归

Miniconda提供快速、简便的Python环境管理&#xff0c;包括安装、运行和更新软件包及其依赖项。Jupyter Notebook是一个交互式笔记本&#xff0c;在机器学习研究中广泛使用。本文旨在进行基础的环境配置&#xff0c;为后续的机器学习实践打好基础。 Miniconda与Jupyter Notebo…

7-简单巡检

KES的版本与license有效期 简单而又会产生灾难性的问题 使用version函数查看KES版本信息 test# select version();查看license有效期 test# select get_license_validdays(); 服务器的时区和时间 查看KES服务器的时区 test# show timezone; test# show time_zone; #两者皆…

【金融风控项目-07】:业务规则挖掘案例

文章目录 1.规则挖掘简介2 规则挖掘案例2.1 案例背景2.2 规则挖掘流程2.3 特征衍生2.4 训练决策树模型2.5 利用结果划分分组 1.规则挖掘简介 两种常见的风险规避手段&#xff1a; AI模型规则 如何使用规则进行风控 **使用一系列逻辑判断(以往从职人员的经验)**对客户群体进行区…

RabbitMQ高可用

生产者确认 生产者确认就是&#xff1a;发送消息的人&#xff0c;要确保消息发送给了消息队列&#xff0c;分别是确保到了交换机&#xff0c;确保到了消息队列这两步。 1、在发送消息服务的application.yml中添加配置 spring:rabbitmq:publisher-confirm-type: correlated …

Redis的特性ubuntu进行安装

文章目录 1.六大特性1.1内存存储数据1.2可编程1.3可扩展1.4持久化1.5集群1.6高可用1.7速度快 2.具体应用场景&#xff08;了解&#xff09;3.Ubuntu安装Redis3.1安装指令3.2查看状态3.3查找配置文件3.4修改文件内容3.5重启服务器生效3.6安装客户端并进行检查 4.Redis客户端介绍…

本地音乐服务器(三)

6. 删除音乐模块设计 6.1 删除单个音乐 1. 请求响应设计 2. 开始实现 首先在musicmapper新增操作 Music findMusicById(int id);int deleteMusicById(int musicId); 其次新增相对应的.xml代码&#xff1a; <select id"findMusicById" resultType"com.exa…

leetcode【滑动窗口】相关题目分析讲解:leetcode209,leetcode904

经典滑动窗口(leetcode209) 题干 题目难度&#xff1a;简单 题目分析 要求找到符合大于等于target的长度最小的子数组的常规思路便是暴力破解——遍历数组&#xff0c;通过两层遍历&#xff0c;找到最小的子数组并返回。 但显而易见&#xff0c;这样时间复杂度会是O(n2)级…

ArkTS组件结构和状态管理

1. 认识基本的组件结构 ArkTS通过装饰器Component 和Entry 装饰 struct 关键字声明的数据结构&#xff0c;构成一个自定义组件 自定义组件中提供了一个build函数&#xff0c;开发者需要在函数内以链式调用的方式进行基本的UI描述&#xff0c;UI描述的方法请参考UI描述规范srtuc…

语义分割(semantic segmentation)

语义分割(semantic segmentation) 文章目录 语义分割(semantic segmentation)图像分割和实例分割代码实现 语义分割指将图片中的每个像素分类到对应的类别&#xff0c;语义区域的标注和预测是 像素级的&#xff0c;语义分割标注的像素级的边界框显然更加精细。应用&#xff1a…

【STM32】在 STM32 USB 设备库添加新的设备类

说实话&#xff0c;我非常想吐槽 STM32 的 USB device library&#xff0c;总感觉很混乱。 USB Device library architecture 根据架构图&#xff1a; Adding a custom class 如果你想添加新的设备类&#xff0c;必须修改的文件有 usbd_desc.cusbd_conf.cusb_device.c 需要…

【母线槽分类与选型】

母线槽是一种高效、安全、节能的输电设备&#xff0c;广泛应用于各类建筑和工业领域。母线槽可以根据不同的分类方式进行划分&#xff0c;例如根据其结构、用途、导体材质等。母线槽以铜或铝作为导体、用非烯性绝缘支撑&#xff0c;然后装到金属槽中而形成的新型导体。在高层建…

一些任务调度的概念杂谈

任务调度 1.什么是调度任务 依赖&#xff1a;依赖管理是整个DAG调度的核心。调度依赖包括依赖策略和依赖区间。 依赖分为任务依赖和作业依赖&#xff0c;任务依赖是DAG任务本身的依赖关系&#xff0c;作业依赖是根据任务依赖每天的作业产生的。两者在数据存储模型上有所不同…

Unifying Top-down and Bottom-up Scanpath Prediction Using Transformers

Abstract 大多数视觉注意力模型旨在预测自上而下或自下而上的控制&#xff0c;这些控制通过不同的视觉搜索和自由观看任务进行研究。本文提出了人类注意力变换器&#xff08;Human Attention Transformer&#xff0c;HAT&#xff09;&#xff0c;这是一个能够预测两种形式注意力…

解决MindSpore-2.4-GPU版本的安装问题

问题背景 虽说在MindSpore-2.3之后的版本中不在正式的发行版中支持GPU硬件后端&#xff0c;但其实在开发分支版本中对GPU后端是有支持的&#xff1a; 但是在安装的过程中可能会遇到一些问题或者报错&#xff0c;这里复现一下我的Ubuntu-20.04环境下的安装过程。 Pip安装 基本的…

【拥抱AI】如何使用BERT等预训练模型计算语义相似度

使用BERT等预训练模型计算语义相似度是一种非常有效的方法&#xff0c;可以捕捉句子之间的深层次语义关系。下面是一个详细的步骤指南&#xff0c;介绍如何使用BERT和Sentence-BERT来计算语义相似度。 1. 环境准备 1.1 安装必要的库 首先&#xff0c;确保你已经安装了必要的…

Excel常用技巧分享

excel单元格内换行 直接按回车会退出当前单元格的编辑&#xff0c;如果需要在单元格中换行&#xff0c;需要按下AltEnter。 excel插入多行或多列 WPS 在WPS中想要插入多行&#xff0c;只需在右键菜单中输入对应的数字即可。 Office Excel excel中相对麻烦一些&#xff0c;比…