欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring 事务采坑-xml注解 事务混用

程序员文章站 2024-03-03 19:37:58
...


为啥使用spring 事务会出现这么多坑?还是对于底层原理实现机制理解不够深刻导致。对于经常使用的东西,不仅仅要求能用,更多的还是需要知道其所以然。

一、坑点分析


1.1 this 调用 本类失效


如果新启动一个事物 Propagation.REQUIRES_NEW 这样的标识,this的直接调用会由于没有走代理的逻辑失效.这一点的理解没有问题 AOP 的机制是实现事务的核心,分布式事务Seata 也是通过AOP去处理,不经过代理准导致事务失效。

1.2 only public 方法事务有效


这个不是经常使用,一般都是xml 配置好的声明事务,对于是否是public的方法很少关注;无论是xml 还是 注解收集最终都是 TransactionAttribute 反映到事务属性里面去。

注解采集


代码地址: org.springframework.transaction.annotation.AnnotationTransactionAttributeSource
publicMethodsOnly – whether to support public methods that carry the Transactional annotation only (typically for use with proxy-based AOP), or protected/private methods as well (typically used with AspectJ class weaving)




AnnotationTransactionAttributeSource 的父类这里当前方法的事务熟悉,根据是否支持pulbic方法,如果yes 不是public 直接返回!


代码地址:org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
		// Don't allow no-public methods as required.
		if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
			return null;
		}
      ......
		return null;
}

xml 配置采集


代码地址: org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource
就如配置一样,通过方法名称 和 TransactionAttribute 对应的键值对信息.




无论是xml 还是 aop 本质上是基于AOP,一般的AOP都是运行时AOP,比较特殊的AspectJ 编译时增强的 AOP 框架直接改写class字节码增强

1.3 部分事务失败导致全局回滚


代码地址: org.springframework.transaction.support.AbstractPlatformTransactionManager#commit
事务的入口程序没有抛出异常要进行提交事务的时候发现,之前执行的已经抛出异常导致部分回滚标识设置为true
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only 这个异常一点十分的熟悉


触发地址:org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback
一个典型的场景如下所示,methodA and methodB 在同一个事务里面,methodB 抛出异常,导致回滚,由于不是新的事务不会直接的执行回滚

部分失败全局回滚示例


这里就会抛出异常 Transaction rolled back because it has been marked as rollback-only

ServiceA {
  @Transactional(propagation = Propagation.PROPAGATION_REQUIRED, rollbackFor = Exception.class)
  void methodA() {
     insertDb();
     try{
         ServiceB.methodB();  
     }catch(Exception e){
         logger.error("异常",e);
     }
  }
}  

ServiceB {  
 @Transactional(propagation = Propagation.PROPAGATION_REQUIRED, rollbackFor = Exception.class)
 void methodB() { 
     throw new BizException("业务异常");
 }  
}

methodB 事务修改


methodB 设置为新事务、挂起不影响入口调用的事务,即使异常了也不会回滚,通常情况下都try catch 了捕获了认为这里挂了不影响我主流程的业务,不应导致全局回滚。

配置部分失败不进行全局回滚


globalRollbackOnParticipationFailure 默认为true

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
        <!-- https://www.iteye.com/blog/jsczxy2-1773795 -->
        <property name="globalRollbackOnParticipationFailure" value="false" />
</bean>


spring的事务中程序控制事务成功失败 可以参考这个连接查看详情。
具体逻辑就是一个老的事务如果出现异常导致进入回滚这里的逻辑,判断是老事务,全局是否是否回滚,如果不尽兴回滚就不设置 getConnectionHolder().setRollbackOnly(); 当前连接回滚的熟悉,这样在事务的入口就commit 检测是否进行全局回滚标识就不会异常了。

//org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback
private void processRollback(DefaultTransactionStatus status) {
    if (status.isNewTransaction()) {
        //新事务执行回滚逻辑
        if (status.isDebug()) {
            logger.debug("Initiating transaction rollback");
        }
        doRollback(status);
    } else if (status.hasTransaction()) {
        //老事务是否设置部分回滚标识
        if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
            //判断是否打开部分回滚失败判断
            if (status.isDebug()) {
                logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
            }
            doSetRollbackOnly(status);
        } else {
            if (status.isDebug()) {
                logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
            }
        }
    }
}
ServiceA {
  @Transactional(propagation = Propagation.PROPAGATION_REQUIRED, rollbackFor = Exception.class)
  void methodA() {
     insertDb();
     try{
         ServiceB.methodB();  
     }catch(Exception e){
         logger.error("异常",e);
     }
  }
}  

ServiceB {  
 @Transactional(propagation = Propagation.PROPAGATION_REQUIRED, rollbackFor = Exception.class)
 void methodB() { 
     throw new BizException("业务异常");
 }  
}

1.4 xml 注解事务混用问题

spring 事务采坑-xml注解 事务混用
先来张图,这张图是故意设置了一个类的调用同时存在xml 和注解事务配置,发挥一下想象力,如果两个org.springframework.transaction.interceptor.TransactionInterceptor 的顺序不一样带来啥样的变化,Propagation 不同带来的变化???感觉都采坑了不少,看了不少的博客,有的通过order的顺序进行解决,由于spring 实现上虽然都是搜集到了TransactionAttribute 但是注解和xml 并没有统一起来导致拦截了两次 order 越小的 越先执行。
下面的示范默认不打开globalRollbackOnParticipationFailure

ServiceA {
  @Transactional(propagation = Propagation.PROPAGATION_REQUIRED, rollbackFor = Exception.class)
  void methodA() {
     insertDb();
     try{
         ServiceB.methodB();  
     }catch(Exception e){
         logger.error("异常",e);
     }
  }
}  

ServiceB {  
 @Transactional(propagation = Propagation.PROPAGATION_NEW, rollbackFor = Exception.class)
 void methodB() { 
     throw new BizException("业务异常");
 }  
}

xml(order 1)->注解(order 2)


xml(PROPAGATION_REQUIRED)
注解(PROPAGATION_NEW)




执行过程分析:methodA 创建一个事务,order 越小越先执行,xml 使用之前的事务,注解创建一个事务,事务回滚,xml 捕获到异常标记全局回滚标识,methodA 提交事务发现全局回滚打标,回滚事务。这里发现如果注解标识的新事务在最外层就没有事。

如果两个都是PROPAGATION_NEW


执行过程分析:methodA 创建一个事务,order 越小越先执行,xml 创建新的事务事务获取连接,注解创建新的事务,获取连接,发现没有如果xml and 注解都是使用的情况下明明一个方法的执行,平白无故的占用了两个连接数。这个解释对?欢迎指点。


解决方案


这个解决方案同事们提供的,都是大牛
在xml 配置表达式中过滤掉类、方法上面有事务注解的

<aop:config>
    <aop:pointcut id="ao_bo"
                    expression="(execution(* com.xyz.myapp.service.*.*(..)))and !(@annotation(org.springframework.transaction.annotation.Transactional) or @within(org.springframework.transaction.annotation.Transactional)))"/>
    <aop:advisor pointcut-ref="ao_bo" advice-ref="defaultTxAdvice"/>
</aop:config>


还有一种解决方案比较复杂,实现将xml 和注解的TransactionAttribute 合并到一起,首先是要注解的,这个其实应该让spring 官方支持一下。
spring声明事务和注解事务并存的问题


1.5 注解获取 TransactionAttribute


@Transactional这个事务注解对继承的问题 @Transactional这个注解继承的问题特别的多,而且不同的版本实现的方式不太一样,但是总体上来说 写在目标类的方法上总没错!
代码地址: org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
第一步先找目标方法的上寻找注解,第二部 然后找targetClass 上寻找注解

private TransactionAttribute computeTransactionAttribute(Method method, Class targetClass) {
    // Don't allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
    }

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
    // If we are dealing with method with generic parameters, find the original method.
    if (JdkVersion.isAtLeastJava15()) {
        specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
    }

    // First try is the method in the target class.
    TransactionAttribute txAtt = findTransactionAttribute(specificMethod);
    if (txAtt != null) {
        return txAtt;
    }

    // Second try is the transaction attribute on the target class.
    txAtt = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAtt != null) {
        return txAtt;
    }

    if (specificMethod != method) {
        // Fallback is to look at the original method.
        txAtt = findTransactionAttribute(method);
        if (txAtt != null) {
            return txAtt;
        }
        // Last fallback is the class of the original method.
        return findTransactionAttribute(method.getDeclaringClass());
    }
    return null;
	}


findTransactionAttribute 最终调用,不同的版本实现不一样。4.x版本是get 5.x是find 差距好大啊!!!
• 4.x 版本 AnnotatedElementUtils.getMergedAnnotationAttributes 只查找当前元素以及元注解(如果是类有继承 @Inherited注解 继承)
• 5.x 版本 AnnotatedElementUtils.findMergedAnnotationAttributes 查找当前类->遍历元注解查找当前注解->查找当前元素的所有接口->查找当前父类方法

public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) {
		AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
				ae, Transactional.class, false, false);
		if (attributes != null) {
			return parseTransactionAnnotation(attributes);
		}
		else {
			return null;
		}
}


自己测试看一下 4.3.20

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public interface AnnotationsService {

    @Transactional(propagation = Propagation.SUPPORTS)
    public void test();

}
public class AnnotationsServiceImpl implements AnnotationsService {

    @Override
    public void test() {

    }
}

@Test
public void testServices() throws Exception {
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setTargetClass(AnnotationsServiceImpl.class);
    proxyFactory.setTarget(new AnnotationsServiceImpl());
    Object proxy = proxyFactory.getProxy();

    Class<?> targetClass = AopUtils.getTargetClass(proxy);

    Method test = ReflectionUtils.findMethod(targetClass, "test");

    AnnotationTransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(true);

    TransactionAttribute transactionAttribute = attributeSource.getTransactionAttribute(test, targetClass);

    log.info("transactionAttribute={}", transactionAttribute.toString());

}

//5.x 22:14:41.184 [main] INFO SpringTest - transactionAttribute=PROPAGATION_SUPPORTS,ISOLATION_DEFAULT; ''
//4.x  null

1.6 特殊异常不回滚


@Transactional 默认情况会对于RuntimeException、Error 进行回滚 仔细看继承图

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

spring 事务采坑-xml注解 事务混用

更多

系列文章一: spring 注解 事务,声明事务共存—有bug
系列文章二:spring 注解 事务,声明事务混用–解决问题
系列文章三:spring 事务采坑-xml注解 事务混用
系列文章四: spring 事务背后的故事
更多汪小哥

相关标签: spring 原理分析