手写spring声明式事务@Transactional(一):手把手解析源码(配gif动态图)

·  阅读 354

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

文章涉及的源码均已上传到了码云,参考【README.md】文件部署运行即可
码云项目:spring-boot源码注释版 git@gitee.com:tangjingshan/spring-boot.git
码云路径:{@link org.springframework.study.dataflow.study_transactional.a_findStrartTraClass#main}

前言

本文主要总结下spring声明式事务的底层核心源码是如何实现的 涉及jar包:

compile project(":spring-jdbc")
compile project(":spring-tx")
implementation group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.7.0'
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.23'
复制代码

涉及表结构:

DROP TABLE IF EXISTS `user_test`;
CREATE TABLE `user_test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
INSERT INTO `user_test` VALUES (1, 'a', 10);
INSERT INTO `user_test` VALUES (2, 'b', 20);
复制代码

相关知识点

1. MethodInterceptor实现aop

使用@Aspect这种方式去实现声明式aop,想必大家都用过了。spring除了注解实现aop,还可以通过实现 MethodInterceptor接口去实现aop,demo如下

public class MethodInterceptorTest {
   public static void main(String[] args) {
      ProxyFactory proxyFactory=new ProxyFactory();
      proxyFactory.setTarget(new MethodInterceptorTest());
      proxyFactory.addAdvice(new TransactionInterceptor());

      Object proxy = proxyFactory.getProxy();
      MethodInterceptorTest methodInterceptor = (MethodInterceptorTest) proxy;

      methodInterceptor.doSomething();
   }

   public void doSomething(){
      System.out.println("开始操作数据库。。。。");
   }

   static class TransactionInterceptor implements MethodInterceptor {
      @Nullable
      @Override
      public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
         System.out.println("方法执行前...");
         Object res = invocation.proceed();
         System.out.println("方法执行后...");
         return res;
      }
   }
}
复制代码

2. ThreadLocal

ThreadLocal当前线程的局部变量,当前线程在java中也不过是一个对象,而ThreadLoacl则就是当前线程对象的一个成员属性,所以不同线程->不同线程对象->ThreadLocal当前线程对象私有

image.png 其可以很便利的解决跨方法传参的问题。比如以下代码需求

son方法接收一个troNo形参,然后调用了father方法,father方法也要用到troNo形参
复制代码

简单的做法如下

static class Test1 {
   public void son(String troNo) {
      System.out.println("son: " + troNo);
      this.father(troNo);
   }

   public void father(String troNo) {
      System.out.println("father: " + troNo);
   }
}
复制代码

此种做法,如果father方法内部又调用了很多其他方法呢,这些方法都有可能用到troNo形参,总不能每一个方法都加上形参troNo吧
这个时候就可以用ThreadLocal来解决

static ThreadLocal<String> localVar = new ThreadLocal<>();

static class Test2 {
   public void son(String troNo) {
      System.out.println("son2: " + troNo);
      localVar.set(troNo);// 绑定当前线程
      this.father();
   }

   public void father() {
      String troNo = localVar.get();// 从当前线程获取值
      System.out.println("father2: " + troNo);
   }
}
复制代码

但是使用ThreadLoacl必须当前线程本次用完后必须要显示的释放资源,如下

try {
   new Test1().son("aaa");
   new Test2().son("bbb");
}finally {
   // 显式清除资源
   localVar.remove();
   //localVar.set(null);
}
复制代码

原因: 因为执行当前方法的当前线程A,是服务器(tomact等)的线程池中的某一个,当前方法执行完后,当前线程A并不会被销毁,而是回到线程池中。所以Thread.ThreadLocal依然存在于线程A,ThreadLocal的值B也不会被GC回收,如果该值B很大,多条线程都持有不可回收的值B,最终就会导致内存泄漏

我们都试过,一个service方法A,调用了另一个service方法B,此时如果方法B失败了,方法A、B都会回滚,但是方法B的形参上却没有显示的接收一个A创建的Connection
ThreadLocal在spring事务中的具体用途,主要就是用来处理Connection的传播,也就是事务传播级别的实现。后面的代码会具体讲到

一. 如何使用

1. 原生命令行版事务

  • 方法1
START TRANSACTION;// 开启事务
INSERT INTO `tjs_mydemo`.`user_test`(`user_name`, `balance`) VALUES ( 'c', 30);
ROLLBACK;// 回滚事务
SELECT * FROM user_test ut;
复制代码
  • 方法2
SET autocommit= 0; //0-关闭自动提交 1/默认-开启自动提交
INSERT INTO `tjs_mydemo`.`user_test`(`user_name`, `balance`) VALUES ( 'd', 40);
ROLLBACK;
SELECT * FROM user_test ut;
复制代码

spring是采用的方法2

2. 原生jdbc版事务

  • 方法1-java版
private static void transactionV1() throws Exception {
   JDBCUtils.printAll("transactionV1之前");
   try (Connection conn = JDBCUtils.getConnection()) {
      try (Statement stmt = conn.createStatement()) {
         String sql = "START TRANSACTION;";
         stmt.execute(sql);
      }
      try (Statement stmt = conn.createStatement()) {
         String sql = "INSERT INTO `tjs_mydemo`.`user_test`(`user_name`, `balance`) VALUES ( 'c3', 110);";
         stmt.execute(sql);
      }
      conn.rollback();
      //throw new RuntimeException("测试回滚。。。");
   } catch (Exception ex) {
      ex.printStackTrace();
   }
   JDBCUtils.printAll("transactionV1之后");
}
复制代码
  • 方法2-java版

仔细看这一段代码,spring声明式事务的核心代码浓缩后也就是这一段

    private static void transactionV2() throws Exception {
      JDBCUtils.printAll("transactionV2之前");
      try (Connection conn = JDBCUtils.getConnection()) {
         conn.setAutoCommit(false);// 这个方法内部也是执行的"SET autocommit= 0;"
//       try (Statement stmt = conn.createStatement()) {
//          String sql = "SET autocommit= 0;";
//          stmt.execute(sql);
//       }
         try (Statement stmt = conn.createStatement()) {
            String sql = "INSERT INTO `tjs_mydemo`.`user_test`(`user_name`, `balance`) VALUES ( 'c2', 120);";
            stmt.execute(sql);
         }
         conn.rollback();
         //throw new RuntimeException("测试回滚。。。");
      } catch (Exception ex) {
         ex.printStackTrace();
      }
      JDBCUtils.printAll("transactionV2之后");
   }
复制代码
  • 完整代码见码云项目

路径:org.springframework.study.dataflow.study_transactional.JDBCTransaction

3. spring声明式事务(测试程序)

springboot声明式事务只是对配置进行了一次封装,最后再说这一块。
先从spring的声明式事务说起。其配置如下

  • 测试类
@Transactional
@Service
public class TransactionalService {
   @Autowired
   JdbcTemplate jdbcTemplate;
   @Autowired
   TransactionalService transactionalService;

   //@Transactional(propagation = Propagation.NEVER)//非事物方式运行
   public void updateSonOfBalance(boolean isThrowException) {
        jdbcTemplate.execute("UPDATE user_test ut SET ut.balance = ut.balance + 1 WHERE ut.id = 1;");
      // 注入的bean,才能走代理
      transactionalService.updateFatherOfUserName(isThrowException);

      // 直接调用updateFatherOfUserName,updateFatherOfUserName是不会走代理的
      // 但是如果此前,已经绑定了已关闭自动提交的Connection到ThreadLocal,则即使没有走代理,也会是在同一事物下
      // this.updateFatherOfUserName(isThrowException);
   }

   //@Transactional(propagation = Propagation.REQUIRES_NEW)//创建一个新事务,如果当前存在事务,将这个事务挂起
   public void updateFatherOfUserName(boolean isThrowException) {
      try {
         jdbcTemplate.execute("UPDATE user_test ut SET ut.user_name = CONCAT(ut.user_name, "_", ut.balance+1)  WHERE ut.id = 1;");
         if (isThrowException) {
            throw new RuntimeException("test rollback.....");
         }
         System.out.println("add success...");
      } catch (Exception ex) {
         System.out.println("add fail, star rollback...");
         throw ex;
      }
   }

   public void find(String pre) {
      RowMapper<UserTest> userInfoRowMapper = new RowMapper<UserTest>() {
         @Override
         public UserTest mapRow(ResultSet rs, int rowNum) throws SQLException {
            UserTest userTest = new UserTest();
            userTest.setId(rs.getInt("id"));
            userTest.setUserName(rs.getString("user_name"));
            userTest.setBalance(rs.getInt("balance"));
            return userTest;
         }
      };
      List<UserTest> userTests = jdbcTemplate.query("select * from user_test ut where ut.id = 1;", userInfoRowMapper);
      System.out.println(pre + ":" + JSON.toJSONString(userTests));
   }
}

public class UserTest implements Serializable {
    private static final long serialVersionUID = 1L;

    private Integer id;
    private String userName;
    private Integer balance;
}
复制代码
  • 配置类
@Configuration
@ComponentScan
@EnableTransactionManagement//开启事物
public class a_findStrartTraClass {
public static void main(String[] args) {
   AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(a_findStrartTraClass.class);

   testUpdate(context);
}

private static void testUpdate(AnnotationConfigApplicationContext context) {
   TransactionalService transactionalService = context.getBean(TransactionalService.class);
   //transactionalService.find("before add");
   try {
      //transactionalService.updateSonOfBalance(true);
      transactionalService.updateFatherOfUserName(true);
   }catch (Exception ex){
      ex.printStackTrace();
      System.out.println("add fail, end rollback...");
   }
   //transactionalService.find("after add");
}

@Bean("jdbcTemplate")
public JdbcTemplate initJdbcTemplate() {
   JdbcTemplate jdbcTemplate = new JdbcTemplate(this.initBasicDataSource());
   return jdbcTemplate;
}
@Bean
public TransactionManager initTransactionManager() {
   org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(this.initBasicDataSource());
   return transactionManager;
}

@Bean("dataSource")
public BasicDataSource initBasicDataSource() {
   BasicDataSource basicDataSource = new BasicDataSource();
   basicDataSource.setUrl("jdbc:mysql://localhost:3306/tjs_mydemo");
   basicDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
   basicDataSource.setUsername("root");
   basicDataSource.setPassword("qx123456");
   return basicDataSource;
}
}
复制代码

二. 制定阅读计划

  • 切入点1

spring声明式事务的相关代码很多,如果你要从@Transactional的代理切面去入手,从上往下去看源码的话,代码的调用链,调用分支将会异常复杂

  • 切入点2

所以,换个切入点。不管怎么样,spring最终还是使用jdk定义的jdbc接口,去调用mysql或其他厂商的具体jdbc接口实现类去操作数据库的
那么,是否可以在jdbc的关键节点的接口打上断点,从下往上,逆推,去研究每一个节点调用链;然后再从上往下,串联所有关键节点,是否会更快速?
仔细观察jdbc事务的java版本代码,可以知道其主要分为下面几个步骤

  1. 创建Connection
{@link java.sql.Driver#connect}
复制代码
  1. 开启事务
{@link java.sql.Connection#setAutoCommit}
复制代码
  1. 创建Statement
{@link java.sql.Connection#createStatement()}
复制代码
  1. 执行目标sql
{@link java.sql.Statement#execute(java.lang.String)}
复制代码
  1. 回滚事务
{@link java.sql.Connection#rollback()}
复制代码
  1. 提交事务
{@link java.sql.Connection#commit}
复制代码
  1. 关闭相关资源
这一步先不管,但是要注意,setAutoCommit(false)时,Connection#close方法,会执行Connection#rollback方法
if (!getAutoCommit() && issueRollback) {
        rollback();
}
复制代码

那么接下来就去寻找这些步骤在spring何处触发

  • 切入点3

切入点2,理论上是可行的,但是还有会有点绕,因为实际操作后会发现,除了你自己的业务sql,spring启动时还会执行一些其他初始化时会调用jdbc相关的api,导致不好调试
所以,再更进一步,spring操作数据库,最终是使用JdbcTemplate去操作的,具体demo见spring声明式事务,我们点进org.springframework.jdbc.core.JdbcTemplate#execute(org.springframework.jdbc.core.StatementCallback<T>, boolean)方法看看里面具体怎么实现的

@Autowired
JdbcTemplate jdbcTemplate;

//@Transactional(propagation = Propagation.NEVER)
public void updateSonOfBalance(boolean isThrowException) {
       jdbcTemplate.execute("UPDATE user_test ut SET ut.balance = ut.balance + 1 WHERE ut.id = 1;");
复制代码

image.png 如果有兴趣,可以点进去以上标红的地方看看,可以发现其最终还是调用的切入点2中说的,mysql对jdk定义的jdbc接口的实现类 所以我们就在标红的地方打上断点,并结合切入点2,开始追踪

PS: 本次研究只研究到截止于jdbc提供的api,这些api具体的源码实现就不在本次研究范围内了

三. 开始阅读

切入点3所标红的三处打上断点,重启测试程序a_findStrartTraClass

1. 创建Connection

1.1 Connection存在哪

   调用链A:
tran_调用链A.gif obtainDataSource()方法只不过返回了数据库连接池的配置实例DataSource,无非就是哪里注入的问题,暂不追究。

image.png 分析调用链A可以发现,Connection是调用ConnectionHolder#getConnection() 方法返回的 所以接下来就是看ConnectionHolder如何获取的,这次不需要重启应用程序,直接使用debug的回退功能,回到调用当前方法前的那行代码,再重新步入即可

   调用链A1:

tran_调用链A1.gif

image.png 分析调用链A1可以发现,ConnectionHolder是存在静态常量org.springframework.transaction.support.TransactionSynchronizationManager#resources中的,其为ThreadLoacl

1.2 该ThreadLoacl何时存入

ThreadLoacl是如何存入值的?大概率是通过ThreadLoacl.set(xxx)赋值的,那么我们全局搜索TransactionSynchronizationManager文件,关键字为resources.set,可以发现只有一处org.springframework.transaction.support.TransactionSynchronizationManager#bindResource调用了,我们再此处打上断点,重启测试程序 image.png

   调用链B:

tran_调用链B.gif image.png

image.png 分析调用链B可以发现,存入threadLocal的值来自于txObject.getConnectionHolder(),既然有getXXX,那么势必就会有setConnectionHolder方法
所以我们就在org.springframework.jdbc.datasource.JdbcTransactionObjectSupport#setConnectionHolder方法打上断点,重启测试程序

   调用链B1:
tran_调用链B1.gif
PS: 这个断点会进两次,第一次为null直接跳过,只分析第二次不为null的那条

image.png 分析调用链B1可以发现,setConnectionHolder方法是在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin处被调用。而且Connection是由数据库连接池javax.sql.DataSource#getConnection()创建的

那么数据库连接池的这个getConnection方法是否会调用前面【切入点2】分析时的{@link java.sql.Driver#connect}这个接口的实现类呢? 我们先在程序doBegin方法里的Connection newCon = obtainDataSource().getConnection();这一行代码打上断点,然后等程序运行到这一行的时候,再在java.sql.Driver#connect打上断点

image.png

   调用链B2:

tran_调用链B2.gif 分析调用链B2可以发现,果然创建Connection最终还是调用了jdk定义的java.sql.Driver#connect接口的,mysql实现类com.mysql.cj.jdbc.NonRegisteringDriver#connect

1.3 doBegin方法何时调用

   继续分析调用链B2,得到调用链B3:
tran_调用链B3.gif image.png 分析调用链B3可以发现,doBegin方法是org.springframework.transaction.interceptor.TransactionInterceptor#invoke处调用的,而TransactionInterceptor实现了MethodInterceptor,为transaction的切面类。

再往上分析,就是spring-aop模块的知识了,与本文研究的主要流程无关,暂时不管,这里就只贴下transaction代理的切点位置

* aop配置类
* {@link org.springframework.transaction.config.TransactionManagementConfigUtils#TRANSACTION_ADVISOR_BEAN_NAME}

* aop切点
* {@link org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut.TransactionAttributeSourceClassFilter#matches(java.lang.Class)}
* {@link org.springframework.transaction.annotation.SpringTransactionAnnotationParser#isCandidateClass(java.lang.Class)}
*     (AnnotationUtils.isCandidateClass(targetClass, Transactional.class);// 是否持有@Transactional注解)
复制代码

1.4 结论

经过前面三大步的分析,逆着总结下,所有数据流向就都串起来了

  1. 判断当前bean是否持有@Transactional注解
{@link org.springframework.transaction.annotation.SpringTransactionAnnotationParser#isCandidateClass(java.lang.Class)}
复制代码
  1. 使用切面类对目标方法增强
{@link org.springframework.transaction.interceptor.TransactionInterceptor#invoke}
复制代码
  1. 使用数据库连接池创建对应数据源的连接Connection
{@link org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin}
    (Connection newCon = obtainDataSource().getConnection();)
复制代码
  1. 下一节补充

  2. 存入ThreadLoacl

{@link org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin}
{@link org.springframework.transaction.support.TransactionSynchronizationManager#bindResource}
    (TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());)
复制代码
  1. 需要获取连接的时候,从ThreadLoacl拿
{@link org.springframework.transaction.support.TransactionSynchronizationManager#doGetResource}
复制代码

2. 开启事务

直接在java.sql.Connection#setAutoCommit这个接口打上断点,重启测试程序即可

   调用链C:

tran_调用链C.gif

分析调用链C可以发现,开启事务的setAutoCommit方法也是在DataSourceTransactionManager#doBegin时,调用org.apache.commons.dbcp2.DelegatingConnection#setAutoCommit方法赋值的。

再整体看下doBegin方法,如下 image.png

2.1 结论

整体分析doBegin方法可以发现,setAutoCommit是在绑定ThreadLocal之前赋值的,所以【1.4】的结论第四点如下

  1. setAutoCommit开启事务
{@link org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin}
{@link org.apache.commons.dbcp2.DelegatingConnection#setAutoCommit}
    (con.setAutoCommit(false);)
复制代码

3. 创建Statement

回到【切入点3】的org.springframework.jdbc.core.JdbcTemplate#execute(org.springframework.jdbc.core.StatementCallback<T>, boolean)方法 image.png 重启测试程序

   调用链D:
tran_调用链D.gif

3.1 结论

分析调用链D可以发现,创建Statement是调用了jdk定义的java.sql.Connection#createStatement()接口的,dbcp2实现类org.apache.commons.dbcp2.DelegatingConnection#createStatement()去创建的(PS:不同的连接池不同的实现)

4. 执行目标sql

继续回到【切入点3】,重启应用程序

   调用链E:
tran_调用链E.gif

4.1 结论

分析调用链E可以发现,执行目标sql是调用了jdk定义的java.sql.Statement#execute(java.lang.String)接口的,dbcp2实现类org.apache.commons.dbcp2.DelegatingStatement#execute(java.lang.String)去执行的

5. 回滚事务

java.sql.Connection#rollback()和抛出异常处打上断点,重启测试程序 image.png image.png

   调用链F:

tran_调用链F.gif
分析调用链F可以发现,执行回滚是调用了jdk定义的java.sql.Connection#rollback()接口的,dbcp2实现类org.apache.commons.dbcp2.DelegatingConnection#rollback()去执行的

DelegatingConnection#rollback()又是在哪里被调用的呢

   继续分析调用链F,得到调用链F1:
tran_调用链F1.gif image.png

5.1 结论

分析调用链F1可以发现,在transaction的切面类,捕捉了调用目标方法时产生的异常,然后调用了DelegatingConnection#rollback()执行的回滚

6. 提交事务

java.sql.Connection#commit()打上断点,并将测试程序改为不抛出异常,重启测试程序 image.png

   调用链G:
tran_调用链G.gif

6.1 结论

分析调用链G可以发现,提交事务是调用了jdk定义的java.sql.Connection#commit()接口的,dbcp2实现类org.apache.commons.dbcp2.DelegatingConnection#commit去执行的.其也是在transaction的切面类,执行完目标方法后执行的。

最后再回过来,总结下切面类,缩减后的核心代码如下 image.png

7. 小结

这里先做下小结,不然后面的阅读将会很吃力
前面6节,已经把单个方法的事务整体流程都走了一遍了,现在逆推总结下,具体总结见5.1 有代码数据流

四. 其他知识点

1. 事务传播级别的源码分析

spring的事务传播级别有很多种,本节以常用的默认级别展开分析

PROPAGATION_REQUIRED:如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行
复制代码

事务传播级别主要是为了解决方法嵌套时事物的处理。何为方法嵌套?比如以下场景 image.png updateSonOfBalance方法内部调用了updateFatherOfUserName方法,此时默认事务传播级别下,这两个方法内对数据库的所有操作都是在一个事务下的
那么接下来请思考以下几个问题

1.1 如何获取Connection

根据前面的1.4 结论的第6点可知,获取Connection直接从ThreadLocal中拿就行了 org.springframework.transaction.support.TransactionSynchronizationManager#doGetResource
那问题来了,为什么调用updateFatherOfUserName方法没有再次创建一个新的Connection呢?是否有一个地方判断了是否为首次调用,首次调用才创建Connection?

根据前面的1.4 结论的第5点可知,Connection是在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin创建的,我们仔细分析下这段代码 image.png 可以发现,Connection是保存在对象txObject的成员属性connectionHolder中的,而且只有该成员属性不为空才会创建一个新的Connection。 既然都围绕着对象txObject,那么我们就追溯下这个对象在哪创建的?在DataSourceTransactionManager#doBegin打上断点,并且把测试程序改成调用updateSonOfBalance方法,重启应用程序 image.png

   调用链H:
tran_调用链H.gif
分析调用链H可以发现,txObject是在调用org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransactionObject transaction = doGetTransaction();时创建的,那么我们在这里打上断点,继续重启应用程序 image.png

   调用链H1:
tran_调用链H1.gif image.png 分析调用链H1可以发现,doGetTransaction主要做以下几件事

  1. 实例化一个新的事物包装类DataSourceTransactionObject

  2. 尝试从ThreadLocal中获取当前Connection,但是获取的为null)

  3. 调用setConnectionHolder赋值为null

       继续调用链H1,得到调用链H2:

tran_调用链H2.gif

分析调用链H2可以发现,doGetTransaction后面的代码主要做以下几件事

  1. 判断txObject.connectionHolder是否为null
  2. 如果为null,就继续判断事物传播级别
  3. 如果需要创建事物,就继续调用DataSourceTransactionManager#doBegin方法
  4. 创建Connection
  5. 绑定Connection到ThreadLoacel
  6. 返回TransactionStatus实例

上面演示的是首次调用时的doGetTransaction,下面再来看看非首次

   调用链H3:
tran_调用链H3.gif

分析调用链H3可以发现,doGetTransaction主要做以下几件事

  1. 实例化一个新的事物包装类DataSourceTransactionObject
  2. 尝试从ThreadLocal中获取当前Connection.A,获取到的值为上一步创建的Connection
  3. 调用setConnectionHolder赋值为Connection.A
  4. 调用org.springframework.transaction.support.AbstractPlatformTransactionManager#handleExistingTransaction方法,返回TransactionStatus实例

1.1.1 结论

至此,Connection的首次和非首次的获取都解析完毕,至于获取后具体在哪里用请看下文 org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction的核心逻辑如下图 image.png

  • 首次
  1. ThreadLocal中获取当前Connection,获取的值为null
  2. 于是新创建了一个Connection,并绑定到当前线程
  • 非首次
  1. ThreadLocal中获取当前Connection,获取的值为上一步创建的Connection

1.2 何时触发回滚事务

根据前面的5.1 回滚事务的结论可知,事务回滚是在DelegatingConnection#rollback()执行的,那么我们在这里打上断点,并且把测试程序改成调用updateSonOfBalance方法,重启应用程序 image.png    调用链I:
tran_调用链I.gif
image.png

分析调用链I可以得到以下类图,此类图很关键,需要背下来
其中TransactionInfo以及其内部成员属性,每一次调用下一个目标方法,都会在进入切面类后,调用目标方法前,实例化一个新的TransactionInfo对象 image.png image.png

调用链I,主要是做了以下几件事

  1. 进入切面类
  2. 构造TransactionInfo实例
  3. 执行目标方法,抛出异常
  4. 判断DefaultTransactionStatus.newTransaction是否true,为true就执行回滚
  5. 获取Connection,具体如何setConnection见调用链B1
  6. 调用Connection#rollback回滚

以上除了第4点外,其他的前面都已经梳理了。现在来看看newTransaction何时赋值。 我们在DefaultTransactionStatus文件全局搜索newTransaction =,可以发现只有一处构造函数调用,我们在这里打上断点,重启测试程序 image.png

   调用链I1:
tran_调用链I1.gif

分析调用链I1可以发现,newTransaction = true是在调用 AbstractPlatformTransactionManager#getTransaction->AbstractPlatformTransactionManager#startTransaction方法,实例化DefaultTransactionStatus时赋值的,该方法后面的代码还会赋值DefaultTransactionStatus.transaction.connectionHolder属性
image.png


根据前面的类图分析可知DefaultTransactionStatus应该会在每次调用目标方法前都会实例化一个新的对象,所以应该在调用updateSonOfBalance,内部调用updateFatherOfUserName方法时还会再次进入DefaultTransactionStatus的构造函数,重启应用程序继续调试

   调用链I2:
tran_调用链I2.gif image.png 分析调用链I2可以发现,内部调用updateFatherOfUserName方法时,newTransaction = true是在调用是在调用 AbstractPlatformTransactionManager#getTransaction->AbstractPlatformTransactionManager#handleExistingTransaction方法赋值的

1.1.2 结论

进入切面类后,在调用每一个目标方法前,会调用TransactionAspectSupport#createTransactionIfNecessary方法构造TransactionInfo实例,只有当TransactionInfo.transactionStatus.newTransaction为true时的那个目标方法执行完,才会执行回滚。
紧接1.1.1 结论,继续总结

  • 首次
  1. ThreadLocal中获取当前Connection,获取的值为null
  2. 实例化DefaultTransactionStatus对象,并将其newTransaction属性置为true
  3. 新创建了一个Connection,并绑定到当前线程
  • 非首次
  1. ThreadLocal中获取当前Connection,获取的值为上一步创建的Connection
  2. 实例化DefaultTransactionStatus对象,并将其newTransaction属性置为false

1.3 何时触发提交事务

根据前面的6.1 提交事务的结论可知,提交事务是在DelegatingConnection#commit()执行的,那么我们在这里打上断点,并且把测试程序改成调用updateSonOfBalance方法且不报错,重启应用程序 image.png

   调用链J:

分析调用链J可以发现,提交事务事务和回滚事务都是一样的,只有在newTransaction属性置为true才执行提交,所以这里就不细化了

1.1.3 结论

1.1.2 结论

1.4 结论

经过上面的分析可知,事务主要是通过包装类TransactionInfo控制的,其具体类图见上文。只有当其newTransaction属性为true才执行提交或者回滚

那么调用下一个目标方法,生成TransactionInfo时控制好其newTransaction属下就可以组合成不同的事务传播级别了。

比如PROPAGATION_REQUIRES_NEW,其定义为

每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行
复制代码

image.png 切面类在调用updateFatherOfUserName前,生成TransactionInfo时,将其newTransaction置为true,并从连接池中重新获取一个新的Connection连接,就可以实现该传播方式了。

其他的传播方式大体类似,嵌套事务可能会还会特殊处理一下回滚点(save point),与本文研究的主要流程无关,暂时不管

五. 总结

5.1 有代码数据流

下面这段代码使用@link注释,复制到你自己的项目中去,ctrl+鼠标左键,即可快速跳转到目标方法,十分方便日后万一遗忘了,快速温习一遍

/**
 **aop配置类
 * {@link org.springframework.transaction.config.TransactionManagementConfigUtils#TRANSACTION_ADVISOR_BEAN_NAME}
 * aop切点
 * {@link org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut.TransactionAttributeSourceClassFilter#matches(java.lang.Class)}
 * {@link org.springframework.transaction.annotation.SpringTransactionAnnotationParser#isCandidateClass(java.lang.Class)}
 *        (AnnotationUtils.isCandidateClass(targetClass, Transactional.class);// 是否持有@Transactional注解)
 *
 **aop切面:
 * {@link TransactionInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)}
 * {@link TransactionAspectSupport#invokeWithinTransaction(java.lang.reflect.Method, java.lang.Class, org.springframework.transaction.interceptor.TransactionAspectSupport.InvocationCallback)}
 *
 **一.构造TransactionInfo
 *        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
 * {@link TransactionAspectSupport#createTransactionIfNecessary(org.springframework.transaction.PlatformTransactionManager, org.springframework.transaction.interceptor.TransactionAttribute, java.lang.String)}
 *        (TransactionStatus status = tm.getTransaction(txAttr);)
 * {@link AbstractPlatformTransactionManager#getTransaction(org.springframework.transaction.TransactionDefinition)}
 *        // fixme 1. 尝试从ThreadLocal中获取事物Connection
 *        Object transaction = doGetTransaction();//非顶级事务才能再这里获取到连接
 *        // fixme 2. 当前线程,是否已经有事物了
 *        if (isExistingTransaction(transaction)) {
 *           // Existing transaction found -> check propagation behavior to find out how to behave.
 *           // fixme 2.1 如果已经有事务了,new TransactionStatus.newTransactionStatus=false
 *           return handleExistingTransaction(def, transaction, debugEnabled);
 *      }
 *      // fixme 2.1 如果没有有事务,则判断事务传播级别
 *      if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||...){
 *             // fixme 2.2 调用doBegin方法,开启一个新事物Connection(con.setAutoCommit(false);),并绑定到ThreadLocal
 *              // new TransactionStatus.newTransactionStatus=true
 *              return startTransaction(def, transaction, debugEnabled, suspendedResources);
 *      }
 *
 **二.调用目标方法
 *        retVal = invocation.proceedWithInvocation();
 *
 **三.执行提交
 *        completeTransactionAfterThrowing(txInfo, ex);
 * {@link TransactionAspectSupport#completeTransactionAfterThrowing(org.springframework.transaction.interceptor.TransactionAspectSupport.TransactionInfo, java.lang.Throwable)}
 * {@link AbstractPlatformTransactionManager#processRollback(org.springframework.transaction.support.DefaultTransactionStatus, boolean)}
 *        if (status.isNewTransaction()) {
 *           //fixme newTransaction为true才回滚
 *           doRollback(status);
 *      }
 *
 **三.执行回滚
 *        commitTransactionAfterReturning(txInfo);
 * {@link TransactionAspectSupport#commitTransactionAfterReturning(org.springframework.transaction.interceptor.TransactionAspectSupport.TransactionInfo)}
 * {@link AbstractPlatformTransactionManager#processCommit(org.springframework.transaction.support.DefaultTransactionStatus)}
 *        if (status.isNewTransaction()) {
 *           //fixme newTransaction为true才提交
 *           doCommit(status);
 *      }
 *
 ** 事务的载体类
 * 顶级类TransactionInfo:{@link TransactionAspectSupport.TransactionInfo}
 * 事务状态类TransactionStatus:{@link DefaultTransactionStatus}
 * 事务包装类transaction:{@link DataSourceTransactionManager.DataSourceTransactionObject}
 * 连接包装类管理类ConnectionHolder:{@link ConnectionHolder}
 * 连接包装类SimpleConnectionHandle:{@link SimpleConnectionHandle}
 *
 ** Connection存储的ThreadLocal
 * 位置:{@link TransactionSynchronizationManager#resources}
 * 获取:{@link TransactionSynchronizationManager#doGetResource(java.lang.Object)}
 * 赋值:{@link TransactionSynchronizationManager#bindResource(java.lang.Object, java.lang.Object)}
 *
 */
复制代码

5.2 无代码流程图

image.png image.png

PS: 此图仅仅为默认事务传播级别下的流程图,如果为Propagation.REQUIRES_NEW或其他,还需要处理下在方法调用结束后,将ThreadLocal中的Connection还原为上一个方法创建的Connection

分类:
后端
标签:
分类:
后端
标签: