@Transactional注解详解(事务失效+源码分析)

1. 本地事务和分布式事务

这部分是题外话,查看@Transactional注解跳过此节

  • 本地事务是指单个数据源(如一个数据库实例或一个消息队列等)内执行的事务操作。数据库系统本身能保证事务的四个ACID特性(原子性、一致性、隔离性和持久性)。代码中使用Spring@Transactional注解就能实现本地事务
  • 分布式事务是指跨数据源(如多个数据库实例、数据库等)之间执行的事务操作。分布式事务管理复杂,需要协调多个参与者来确保事务的一致性,常见的协调机制两阶段提交,三阶段提交,TCC,XA等。由于网络延迟、系统故障等问题,分布式事务难以严格保证 ACID 特性。很多系统选择牺牲某些属性(如短暂不一致)来提高系统的可用性,系统允许某一时刻数据不一致,但是通过补偿或者重试机制,最终达到最终一致性

注意到本地事务是单个数据源,所以说分库分表中,有一种逻辑分库使用本地事务就能解决。因为逻辑分库只是将表放在不同地数据库,而这些数据库任然运行在同一个数据库实例中,对应地物理分库就需要分布式事务解决了

2. @Transactional注解使用

spring中事务解决方案有两种,编程式事务和声明式事务,@Transactional就是声明式事务地解决方案

@Tranasctional注解是Spring 框架提供的声明式注解事务解决方案,我们在开发中使用事务保证方法对数据库操作的原子性,要么全部成功,要么全部失败,对应地只需要在方法或类中添加@Tranasctional即可。

常见的使用方式:使用rollbackFor 属性来定义回滚的异常类型,使用 propagation 属性定义事务的传播行为。

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void processPayment(Order order) throws Exception {
    // 如果抛出 Exception 或其子类的异常,将回滚事务
    if (paymentFailed()) {
        throw new Exception("Payment failed");
    }
}

@Transactional 注解只能回滚非检查型异常,具体为RuntimeException及其子类和Error子类,可以从Spring源码的DefaultTransactionAttribute类里找到判断方法rollbackOn

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

2.1 @Transactional属性

  • propagation(事务传播行为)
  • isolation(事务隔离级别)
  • timeout(事务超时时间)
  • readOnly(是否只读)
  • rollbackFor(回滚条件,指定哪些异常会触发回滚)
  • noRollbackFor(不回滚条件,指定哪些异常不会触发回滚)
  • rollbackForClassName(根据异常类名回滚)
  • noRollbackForClassName(根据异常类名不回滚)
  • transactionManager(自定义事务管理器)
  • value(别名,等同于 transactionManager

2.2 @Transactional事务失效

使用@Transactional注解时可能会出现事务失效:

  1. 底层数据库本身不支持事务

  2. @Transactional 注解只能用在public 方法上,如果用在protected或者private的方法上,不会报错,但是该注解不会生效。

  3. @Transactional注解不能回滚被try{}catch() 捕获的异常。

  4. @Transactional注解只能对在被Spring容器扫描到的类下的方法生效。

  5. 类内部调用调用类内部@Transactional标注的事务方法,事务失效

    public class Test{
      public void A(){
        //插入一条数据
        //调用B方法
        B();
      }
      
      @Transactional
      public void B(){
        //插入数据
      }
    }
    

    为什么会失效呢?其实原因很简单,Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。

2.3 @Transactional事务失效分析

这部分为摘录他人笔记整理!!!@Transactional注解不起作用解决办法及原理分析_transactional注解作用异常-CSDN博客

主要时分析@Transactional标识非public方法,或者类内部调用事务方法,事务方法异常被try{}catch

原因简述:@Transactional事务管理基于动态代理实现

  1. @Transactional标识非public方法,spring将不会对bean进行代理对象创建或者不会对方法进行代理调用
  2. 类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,而是直接通过this对象来调用方法,绕过了代理对象,肯定就是没有代理逻辑
  3. 注解没有捕获异常,相应事务方法失效

2.3.1 非public方法事务失效

@Transactional注解标注方法修饰符为非public时,@Transactional注解将会不起作用。这里分析 的原因是,@Transactional是基于动态代理实现的,@Transactional注解实现原理中分析了实现方法,在bean初始化过程中,对含有@Transactional标注的bean实例创建代理对象,这里就存在一个spring扫描@Transactional注解信息的过程,不幸的是源码中体现,标注@Transactional的方法如果修饰符不是public,那么就默认方法的@Transactional信息为空,那么将不会对bean进行代理对象创建或者不会对方法进行代理调用

@Transactional注解实现原理中,介绍了如何判定一个bean是否创建代理对象,大概逻辑是。根据spring创建好一个aop切点BeanFactoryTransactionAttributeSourceAdvisor实例,遍历当前bean的class的方法对象,判断方法上面的注解信息是否包含@Transactional,如果bean任何一个方法包含@Transactional注解信息,那么就是适配这个BeanFactoryTransactionAttributeSourceAdvisor切点。则需要创建代理对象,然后代理逻辑为我们管理事务开闭逻辑。

spring源码中,在拦截bean的创建过程,寻找bean适配的切点时,运用到下面的方法,目的就是寻找方法上面的@Transactional信息,如果有,就表示切点BeanFactoryTransactionAttributeSourceAdvisor能够应用(canApply)到bean中,

AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class<?>, boolean)

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
   Assert.notNull(pc, "Pointcut must not be null");
   if (!pc.getClassFilter().matches(targetClass)) {
      return false;
   }
 
   MethodMatcher methodMatcher = pc.getMethodMatcher();
   if (methodMatcher == MethodMatcher.TRUE) {
      // No need to iterate the methods if we're matching any method anyway...
      return true;
   }
 
   IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
   if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
      introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
   }
 
    //遍历class的方法对象
   Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
   classes.add(targetClass);
   for (Class<?> clazz : classes) {
      Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
      for (Method method : methods) {
         if ((introductionAwareMethodMatcher != null &&
               introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
             //适配查询方法上的@Transactional注解信息  
             methodMatcher.matches(method, targetClass)) {
            return true;
         }
      }
   }
 
   return false;
}

我们可以在上面的方法打断点,一步一步调试跟踪代码,最终上面的代码还会调用如下方法来判断。在下面的方法上断点,回头看看方法调用堆栈也是不错的方式跟踪

  • AbstractFallbackTransactionAttributeSource#getTransactionAttribute
  • AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
   // Don't allow no-public methods as required.
   //非public 方法,返回@Transactional信息一律是null
   if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
   }
   //后面省略.......
 }

@Transactional注解标识非public方法,spring不会创建代理对象或者不进行代理调用导致事务失效

第一种:不创建代理对象

测试代码:

/**
 * @author zhoujy
 **/
@Component
public class TestServiceImpl {
    @Resource
    TestMapper testMapper;
    
    @Transactional
    void insertTestWrongModifier() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }
 
}

@Component
public class InvokcationService {
    @Resource
    private TestServiceImpl testService;
    public void invokeInsertTestWrongModifier(){
        //调用@Transactional标注的默认访问符方法
        testService.insertTestWrongModifier();
    }
}

//测试用例
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
   @Resource
   InvokcationService invokcationService;
 
   @Test
   public void  testInvoke(){
      invokcationService.invokeInsertTestWrongModifier();
   }
}

所以,如果所有方法上的修饰符都是非public的时候,那么将不会创建代理对象。以上述测试代码为例,如果正常的修饰符的testService是下面图片中的,经过cglib创建的代理对象,如下:

image-20240922223818045

如果class中的方法都是非public的那么将不是代理对象。

image-20240922223955034

第二种:不进行代理调用

考虑一种情况,如下面代码所示。两个方法都被@Transactional注解标注,但是一个有public修饰符一个没有,那么这种情况我们可以预见的话,一定会创建代理对象,因为至少有一个public修饰符的@Transactional注解标注方法。

创建了代理对象,insertTestWrongModifier就会开启事务吗?答案是不会。

/**
 * @author zhoujy
 **/
@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;
 
    @Override
    @Transactional
    public void insertTest() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }
    
    @Transactional
    void insertTestWrongModifier() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }
}

原因是在动态代理对象进行代理逻辑调用时,在cglib创建的代理对象的拦截函数中CglibAopProxy.DynamicAdvisedInterceptor#intercept,有一个逻辑如下,目的是获取当前被代理对象的当前需要执行的method适配的aop逻辑。

List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

而针对@Transactional注解查找aop逻辑过程,相似地,也是执行一次

  • AbstractFallbackTransactionAttributeSource#getTransactionAttribute
  • AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

也就是说还需要找一个方法上的@Transactional注解信息,没有的话就不执行代理@Transactional对应的代理逻辑,直接执行方法。没有了@Transactional注解代理逻辑,就无法开启事务

2.3.2 类内部调用事务方法

在类内部调用调用类内部@Transactional标注的方法。这种情况下也会导致事务不开启。

经过对第一种的详细分析,对这种情况为何不开启事务管理,原因应该也能猜到;

既然事务管理是基于动态代理对象的代理逻辑实现的,那么如果在类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,而是直接通过this对象来调用方法,绕过的代理对象,肯定就是没有代理逻辑了。

其实我们可以这样玩,内部调用也能实现开启事务,代码如下。

/**
 * @author zhoujy
 **/
@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;
 
    @Resource
    TestServiceImpl testServiceImpl;
 
 
    @Transactional
    public void insertTestInnerInvoke() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }
 
 
    public void testInnerInvoke(){
        //内部调用事务方法
        testServiceImpl.insertTestInnerInvoke();
    }
 
}

上面就是使用了代理对象进行事务调用,所以能够开启事务管理,但是实际操作中,没人会闲的蛋疼这样子玩~

2.3.3 事务方法中异常被捕获

事务方法内部捕捉了异常,没有抛出新的异常,导致事务操作不会进行回滚。

这种的话,可能我们比较常见,问题就出在代理逻辑中,我们先看看源码里卖弄动态代理逻辑是如何为我们管理事务的。

TransactionAspectSupport#invokeWithinTransaction

代码如下

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
      throws Throwable {
 
   // If the transaction attribute is null, the method is non-transactional.
   final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
   final PlatformTransactionManager tm = determineTransactionManager(txAttr);
   final String joinpointIdentification = methodIdentification(method, targetClass);
 
   if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
      // Standard transaction demarcation with getTransaction and commit/rollback calls.
       //开启事务
      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
      Object retVal = null;
      try {
         // This is an around advice: Invoke the next interceptor in the chain.
         // This will normally result in a target object being invoked.
          //反射调用业务方法
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
         // target invocation exception
          //异常时,在catch逻辑中回滚事务
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }
       //提交事务
      commitTransactionAfterReturning(txInfo);
      return retVal;
   }
 
   else {
     //....................
   }
}

所以看了上面的代码就一目了然了,事务想要回滚,必须能够在这里捕捉到异常才行,如果异常中途被捕捉掉,那么事务将不会回滚

3. Spring事务的传播行为

事务的传播行为指在方法调用过程中,调用方法和被调用方法的事务如何在传播或管理。换句话说,一个事务方法里调用另一个方法A,那么方法A的事务如何处理?

Spring事务的传播行为一共有7种,定义在spring-tx模块的Propagation枚举类里,常量分别对应int0~6

PROPAGATION_REQUIRED支持当前事务,如果当前没有事务,则创建一个事务,这是最常见的选择。
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务来执行
PROPAGATION_MANDATORY支持当前事务,如果没有当前事务,就抛出异常。
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务执行操作,如果当前存在事务,则当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED 类似的操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值