当前位置: 首页 > article >正文

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转换为 PlatformTransactionManager
    PlatformTransactionManager 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 {

        @Override
        public void m1() {
            System.out.println("m1");

        }

        @Override
        public 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$$eed200b8
	at 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

      @Override
	public 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,最后发现本地是跟上次的情况一样,创建了嵌套代理。


    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);

        return defaultAdvisorAutoProxyCreator;
    }

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

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

   @Autowired
    private DataSource dataSource;


    @Resource
    private DataSource masterDataSource;
    @Test
    public 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.rollback
            transactionManager.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.kler.cn/a/403099.html

相关文章:

  • 大模型呼入机器人系统如何建设?
  • el-select 和el-tree二次封装
  • 【Python TensorFlow】进阶指南(续篇三)
  • 网络安全-企业环境渗透2-wordpress任意文件读FFmpeg任意文件读
  • mac安装Pytest、Allure、brew
  • 飞凌嵌入式T113-i开发板RISC-V核的实时应用方案
  • wife_wife
  • 设计探测1飞伏的装置可能吗?
  • gitlab ci/cd搭建及使用笔记(三)
  • 常见协议所对应的漏洞
  • 如何在 Ubuntu 上使用 Docker 部署 LibreOffice Online
  • 基于isSpring的PPT转换
  • 计算机视觉中的双边滤波:经典案例与Python代码解析
  • Win本地部署大模型推理API封装调用
  • 关于win11电脑连接wifi的同时,开启热点供其它设备连接
  • lua脚本使用redis
  • word设置交叉引用快捷键和居中快捷键
  • Streamlit + AI大模型API实现视频字幕提取
  • 统计机器学习——线性回归与分类
  • CSS 3D球形旋转
  • shell脚本2---清风
  • StructRAG Boosting Knowledge 论文笔记
  • Genuine-OJ 是一个现代化的在线评测系统(Online Judge, OJ)
  • 计算机毕业设计 | SpringBoot+vue汽车资讯网站 汽车购买咨询管理系统(附源码+论文)
  • Android开发实战班 -应用架构 - MVVM 架构模式
  • TCP Analysis Flags 之 TCP Dup ACK