【从源码级别理解让人抓狂的@Transactional注解】

楔子

小七最近经常遇到小伙伴对@Transactional的使用有诸多疑问,比如@Transactional什么时候生效,@Transactional什么时候不生效,还有些小伙伴说@Transactional会吃掉代码的异常,错误抛不出来,弄得小七都差点怀疑自己了。所以小七决定通过这篇文章好好分析一下@Transactional。

一些常见的场景

首先大家都知道,@Transactional是基于AOP的,那么AOP失效的场景,@Transactional也就会失效了,不需要去记每一种失效的场景。我们通过下面例子,加深一下理解。

公共代码

我们这里为了方便测试使用的是Mybatis-Plus

Controll层

@RestController
@RequestMapping(value = "/user",produces = "text/html;charset=utf-8")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 测试Mybatis-Plus 新增
     */
    @GetMapping("/save")
    public void save() {
        User user = new User();
        boolean save = userService.save(user);
    }
}

Service接口

public interface UserService extends IService<User> {
       void save(User user) throws RuntimeException;
}

Service实现类

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    UserService userService;

    @Override
    public void save(User user) throws RuntimeException {
        
    }

}

场景一

场景一:
用户——>调用Controller(bean)的save方法——>调用Service的mySave方法(该方法上有@Transactional注解)——>调用doSave方法,向数据库插入数据

代码

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    UserService userService;

    @Override
    @Transactional
    public void mySave(User user) throws RuntimeException {
        doSave(user);
    }
    
    public void doSave(User user){
        super.save(user);
    }

}

分析

谁调用的加了注解的方法?Spring的bean调用的,有事务;事务范围Service的mySave方法

场景二

场景二:
用户——>调用Controller(bean)的save方法——>调用Service的mySave方法——>this(当前类的对象)调用doSave方法(该方法上有@Transactional注解),向数据库插入数据

代码

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    UserService userService;

    @Override
    public void mySave(User user) throws RuntimeException {
        this.doSave(user);
    }
    
    @Transactional
    public void doSave(User user){
        super.save(user);
    }

}

分析

谁调用的加了注解的方法?当前对象调用的,没事务

场景三

场景三:
用户——>调用Controller(bean)的save方法——>调用Service的mySave方法——>service(bean)调用doSave方法(该方法上有@Transactional注解),向数据库插入数据

代码

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    UserService userService;

    @Override
    public void mySave(User user) throws RuntimeException {
        userService.doSave(user);
    }
    
    @Transactional
    public void doSave(User user){
        super.save(user);
    }

}

分析

谁调用的加了注解的方法?bean调用的,有事务;事务范围Service的doSave方法

场景四

场景四:
用户——>调用Controller(bean)的save方法——>调用Service的mySave方法(该方法上有@Transactional注解)——>this(当前类的对象)调用doSave方法,向数据库插入数据

代码

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    UserService userService;

    @Override
    @Transactional
    public void mySave(User user) throws RuntimeException {
        this.doSave(user);
    }
    
    public void doSave(User user){
        super.save(user);
    }

}

分析

谁调用的加了注解的方法?bean调用的,有事务;事务范围Service的mySave方法

场景五

场景五:
用户——>调用Controller(bean)的save方法——>调用Service的mySave方法(该方法上有@Transactional注解)——>this(当前类的对象)调用doSave方法(该方法为私有方法),向数据库插入数据

代码

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    UserService userService;

    @Override
    @Transactional
    public void mySave(User user) throws RuntimeException {
        this.doSave(user);
    }
    
    private void doSave(User user){
        super.save(user);
    }

}

问题

问:事务能生效吗?

小结

AOP失效的场景,@Transactional也会失效。

源码分析

如果是代理对象,调用事务方法,那么会走下面的增强方法

TransactionInterceptor#invoke

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
   // Work out the target class: may be {@code null}.
   // The TransactionAttributeSource should be passed the target class
   // as well as the method, which may be from an interface.
   Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

   // Adapt to TransactionAspectSupport's invokeWithinTransaction...
   return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

具体逻辑在这个方法中

TransactionAspectSupport#invokeWithinTransaction

@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {

   // If the transaction attribute is null, the method is non-transactional.
   TransactionAttributeSource tas = getTransactionAttributeSource();
   // 1、获取对应事务属性
   final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
   // 2、获取 beanFactory 中的 transactionManage,确定事务管理器
   final PlatformTransactionManager tm = determineTransactionManager(txAttr);
   // 获取需要被代理的方法
   final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
   // 3、如果是声明式事务 @Transactional,则走这段逻辑
   if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
      // Standard transaction demarcation with getTransaction and commit/rollback calls.
      // 4、创建 TransactionInfo
      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.
         // 5、执行目标的方法
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
         // target invocation exception
         // 6、异常处理
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         // 7、提交事务前清除事务信息
         cleanupTransactionInfo(txInfo);
      }
      // 8、提交事务
      commitTransactionAfterReturning(txInfo);
      return retVal;
   }
   // 3、如果是编程式事务,则走下面逻辑。这里我们忽略他
   else {
     .....
   }
}

综上所述,@Transactional的处理步骤大致如下:

  1. 获取事务属性。
  2. 确定事务管理器,加载配置中配置的TransactionManager。
  3. 不同的事务处理方式使用不同的逻辑。
  4. 创建 TransactionInfo,为了在目标方法执行前获取事务并且收集事务信息。
  5. 执行目标方法。
  6. 异常处理。
  7. 提交事务前清除事务信息。
  8. 提交事务。
    接下来看看创建TransactionInfo的方法

TransactionAspectSupport#createTransactionIfNecessary

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
      @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

   // If no name specified, apply method identification as transaction name.
   if (txAttr != null && txAttr.getName() == null) {
      txAttr = new DelegatingTransactionAttribute(txAttr) {
         @Override
         public String getName() {
            return joinpointIdentification;
         }
      };
   }

   TransactionStatus status = null;
   if (txAttr != null) {
      if (tm != null) {
         // 获取事务信息
         status = tm.getTransaction(txAttr);
      }
      else {
         if (logger.isDebugEnabled()) {
            logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
                  "] because no transaction manager has been configured");
         }
      }
   }
   // 构建事务信息
   return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

该方法主要做了两件事情

  1. 获取事务。
  2. 构建事务信息。
    获取事务信息的方法

AbstractPlatformTransactionManager#getTransaction

@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
   // 真正的获取 transaction的方法
   Object transaction = doGetTransaction();

   // Cache debug flag to avoid repeated checks.
   boolean debugEnabled = logger.isDebugEnabled();

   if (definition == null) {
      // Use defaults if no transaction definition given.
      definition = new DefaultTransactionDefinition();
   }
   // 判断当前线程是否存在事务
   if (isExistingTransaction(transaction)) {
      // Existing transaction found -> check propagation behavior to find out how to behave.
      return handleExistingTransaction(definition, transaction, debugEnabled);
   }

   // Check definition settings for new transaction
   // 验证事务超时的设置
   if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
      throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
   }

   // No existing transaction found -> check propagation behavior to find out how to proceed.
   if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
      throw new IllegalTransactionStateException(
            "No existing transaction found for transaction marked with propagation 'mandatory'");
   }
   else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
         definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
         definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
      SuspendedResourcesHolder suspendedResources = suspend(null);
      if (debugEnabled) {
         logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
      }
      try {
         boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
         DefaultTransactionStatus status = newTransactionStatus(
               definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
         // 这个方法很重要,后面会具体分析,可以说事务就是从这个方法开始的
         doBegin(transaction, definition);
         // 根据需要初始化事务同步
         prepareSynchronization(status, definition);
         return status;
      }
      catch (RuntimeException | Error ex) {
         resume(null, suspendedResources);
         throw ex;
      }
   }
   else {
      ......
   }
}

跟进doBegin方法

DataSourceTransactionManager#doBegin

protected void doBegin(Object transaction, TransactionDefinition definition) {
   DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
   Connection con = null;

   try {
      // 1、判断是否复用当前线程连接
      if (!txObject.hasConnectionHolder() ||
            txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
         Connection newCon = obtainDataSource().getConnection();
         if (logger.isDebugEnabled()) {
            logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
         }
         txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
      }

      txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
      con = txObject.getConnectionHolder().getConnection();
      // 2、设置隔离级别
      Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
      txObject.setPreviousIsolationLevel(previousIsolationLevel);

      // 如有必要,切换到手动提交。
      // 这在一些 JDBC 驱动程序中非常昂贵,
      // 所以我们不想不必要地这样做(例如,如果我们已经显式配置连接池以设置它)。
      // 3、设置手动提交,并交给spring管理
      if (con.getAutoCommit()) {
         txObject.setMustRestoreAutoCommit(true);
         if (logger.isDebugEnabled()) {
            logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
         }
         con.setAutoCommit(false);
      }
      // 4、设置事务是否激活为true
      prepareTransactionalConnection(con, definition);
      txObject.getConnectionHolder().setTransactionActive(true);

      int timeout = determineTimeout(definition);
      // 5、设置超时时间
      if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
         txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
      }

      // 6、将连接绑定到当前线程
      if (txObject.isNewConnectionHolder()) {
         TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
      }
   }

   catch (Throwable ex) {
      if (txObject.isNewConnectionHolder()) {
         DataSourceUtils.releaseConnection(con, obtainDataSource());
         txObject.setConnectionHolder(null, false);
      }
      throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
   }
}

综上所述,我们调用doBegin方法时,大致流程如下:

  1. 判断是否复用当前线程连接
  2. 设置隔离级别
  3. 设置手动提交,并交给spring管理
  4. 设置事务是否激活为true
  5. 设置超时时间
  6. 将连接绑定到当前线程
    这一条线走完了,让我们回到

TransactionAspectSupport#createTransactionIfNecessary#prepareTransactionInfo

protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm,
      @Nullable TransactionAttribute txAttr, String joinpointIdentification,
      @Nullable TransactionStatus status) {

   TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);
   if (txAttr != null) {
      // We need a transaction for this method...
      if (logger.isTraceEnabled()) {
         logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]");
      }
      // 如果不兼容的 tx 已经存在,事务管理器将标记错误。
      txInfo.newTransactionStatus(status);
   }
   else {
      // The TransactionInfo.hasTransaction() method will return false. We created it only
      // to preserve the integrity of the ThreadLocal stack maintained in this class.
      if (logger.isTraceEnabled()) {
         logger.trace("Don't need to create transaction for [" + joinpointIdentification +
               "]: This method isn't transactional.");
      }
   }

   // We always bind the TransactionInfo to the thread, even if we didn't create
   // a new transaction here. This guarantees that the TransactionInfo stack
   // will be managed correctly even if no transaction was created by this aspect.
   txInfo.bindToThread();
   return txInfo;
}

该方法就是构造了一个TransactionInfo对象,并将他绑定给了线程。
接着我们回到最初的方法

TransactionAspectSupport#invokeWithinTransaction
第五步执行了目标方法,如果报错,那么会调用以下方法

TransactionAspectSupport#completeTransactionAfterThrowing

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
   if (txInfo != null && txInfo.getTransactionStatus() != null) {
      if (logger.isTraceEnabled()) {
         logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
               "] after exception: " + ex);
      }
      // 这里是@Transactional默认哪种异常会尝试进行回滚
      if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
         try {
            txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
         }
         catch (TransactionSystemException ex2) {
            logger.error("Application exception overridden by rollback exception", ex);
            ex2.initApplicationException(ex);
            throw ex2;
         }
         catch (RuntimeException | Error ex2) {
            logger.error("Application exception overridden by rollback exception", ex);
            throw ex2;
         }
      }
      else {
         // We don't roll back on this exception.
         // Will still roll back if TransactionStatus.isRollbackOnly() is true.
         try {
            txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
         }
         catch (TransactionSystemException ex2) {
            logger.error("Application exception overridden by commit exception", ex);
            ex2.initApplicationException(ex);
            throw ex2;
         }
         catch (RuntimeException | Error ex2) {
            logger.error("Application exception overridden by commit exception", ex);
            throw ex2;
         }
      }
   }
}

默认调用的回滚配置

DefaultTransactionAttribute#rollbackOn

public boolean rollbackOn(Throwable ex) {
   // 默认运行时异常,或者error才回滚
   return (ex instanceof RuntimeException || ex instanceof Error);
}

源码调试

我们针对场景五进行调试

关键依赖版本如下:

<!--spring-web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

阅读静态源码后,标记的断点如下:

在这里插入图片描述

结果:

调用Service的mySave方法(该方法上有@Transactional注解)的时候,就进入了invoke方法,事务生效了。

例子地址

这里不提供完整代码例子,提供mybatis-plus的半成品小例,读者可以自行修改尝试。

https://gitee.com/diqirenge/sheep-web-demo/tree/master/sheep-web-demo-mybatis-plus

从生产问题再看@Transactional

这是小七在生产中遇到的一个问题,大体上是为一个第三方服务增加一个重试和兜底方案。因为只是一个单纯的http调用,小七第一时间就想到了spring的retry框架,结果用上@retry后,非但没有重试,反而报数据库表被锁的异常。查看了以前的代码,发现正常流程下,代码是ok的,但是异常流程直接GG,遂做以下记录。

例子

伪代码如下:

@Transactional
public void mytest() throws InterruptedException {
    System.out.println("开始执行主线程逻辑...");
    User user = new User();
    user.setName("测试");
    user.setPassword("1222");
    super.save(user);
  
    // 错误一:另起线程相当于,另外开启了一个事务,一旦发生异常不会回滚主线程的事务
    ThreadPoolUtil.execute(()->{
        // 错误二:该方法中再次对user表进行了操作(不同事务操作同一张表,极易造成锁表)
        this.mytest1(user);
    });

    // 模拟主线程剩下的操作(请求三方接口,http连接保持一分钟)
    // 错误三:结合错误二来看,主线程长时间不提交事务,子线程也不能提交事务,最后会造成数据库被锁
    Thread.sleep(60000);
    System.out.println("主线程逻辑执行完毕...");
}
@Transactional
private void mytest1(User user){
    // 一系列操作复杂逻辑操作
    System.out.println("开始执行子线程逻辑...");
    user.setName("测试2");
    super.updateById(user);
    if (1==1){
        throw new RuntimeException("更新操作出错");
    }
    System.out.println("开始执行子线程执行完成...");
}

小结

1、尽量避免在事务中远程调用等响应时间过长的方法

2、避免AOP失效的场景下使用@Transactional

加餐

@Transactional与@Test一起使用的时候

这个场景下会默认rollback=true,也就是说,不管有没有异常,事务都是会回滚的

在这里插入图片描述

@Transactional标记的方法有锁的时候

伪代码如下:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    UserService userService;

    @Override
    @Transactional
    public void mySave(User user) throws RuntimeException {
        this.doSave(user);
    }
    
    private synchronized void doSave(User user){
        super.save(user);
    }

}

这个时候锁是有可能会失效的,特别是在高并发的情况下。为什么呢?通过源码我们可以知道,
synchronized代码块里执行的内容是在事务里面的,在事务commit前可能会有多个线程进入代码块,导致读取的数据都是一致的,不是更新后的,也就是说事务的范围大于了锁的范围,造成锁失效的情况。

怎么解决这个问题呢?要么升级锁的范围,要么降低事务的范围。但是如果真的遇到了这种问题,多半是设计不合理,小七觉得最好还是好好梳理一下代码,看看怎么重构好。

Transaction rolled back because it has been marked as rollback-only

伪代码如下:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    UserService userService;

    @Override
    @Transactional
    public void mySave(User user) throws RuntimeException {
          super.save(user);
          try {
              userService.doSave(user);
          } catch (Exception e) {
              e.printStackTrace();
         }
    }
    
    @Transactional
    public void doSave(User user){
        super.save(user);
        System.out.println(1/0);
    }

}

事物的传播性默认为Propagation.REQUIRED,如果有事务存在,则加入原事务,如果不存在则开启新事务。

这里我们可以将mySave和doSave看成同一个事务中。

如果doSave方法发生异常,那么这个事务被标记成要回滚了,也就是说doSave是想要回滚的,但是mySave中却将这个异常捕获了,那么mySave肯定会想要提交事务,你说这个时候Spring能怎么办?他肯定是懵逼的,不得不说Spring的作者是真牛逼,他早就想到了这种情况,于是Spring的作者写了以下代码:

AbstractPlatformTransactionManager#processRollback

// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
   throw new UnexpectedRollbackException(
         "Transaction rolled back because it has been marked as rollback-only");
}

这就是事务嵌套抛出异常的原因。

总​结

本文只是一个对@Transactional的简单入门,带着读者初略的看了一下源码,至于很多细节都没涉及,因为篇幅有限,感兴趣的读者,可以自行阅读。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

第七人格

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值