【重写SpringFramework】Spring事务原理(chapter 4-3)

133 阅读14分钟

1. 前言

在传统的事务结构中,外层的方法控制 Connection 执行相关的事务操作,内层的方法完成具体的 JDBC 操作。如下图所示,绿色区域代表外层方法,内层方法 step1step2 都置于同一个事务当中。在执行内层方法前必定开启事务,在内层方法执行完后提交或回滚事务,这些步骤都是固定的。默认情况下,内层的方法都属于同一个事务,如果某个方法想脱离当前事务,或者开启新的事务,处理起来会比较麻烦。

3.1 事务方法的结构.png

Spring 事务要对传统的事务结构进行改造,首先要破除外层方法的大包大揽,将每个内层方法都独立出来,让其自行决定是否开启事务、以及提交和回滚。这样一来,各个方法可以属于同一个事务,也可以分属不同的事务,灵活性大为提高。因此,构建 Spring 事务的关键在于将事务方法之间的纵向关系改成横向关系

2. 事务定义

TransactionDefinition 接口定义了一组获取事务属性的方法,类似 BeanDefinition 的作用。

public interface TransactionDefinition {
    int getPropagationBehavior();
    int getIsolationLevel();
    int getTimeout();
    boolean isReadOnly();
    String getName();
}

DefaultTransactionDefinition 作为 TransactionDefinition 接口的默认实现类,定义了相关的事务属性,如下所示:

  • propagationBehavior:事务的传播行为,即多个方法之间事务是如何进行传递的(后续介绍)
  • isolationLevel:事务的隔离级别
  • timeout:事务的超时时间,默认-1,永不超时
  • readOnly:是否为只读事务,默认 false
  • name:当前方法的事务名称
public class DefaultTransactionDefinition implements TransactionDefinition {
    private int propagationBehavior = PROPAGATION_REQUIRED;
    private int isolationLevel = ISOLATION_DEFAULT;
    private int timeout = TIMEOUT_DEFAULT;
    private boolean readOnly = false;
    private String name;
}

3. 事务对象

SmartTransactionObject 接口表示一个事务对象,主要作用是管理一个内部的 rollbackOnly 标记。接口名中的 Smart,其语义主要体现在如何处理回滚。考虑这样一种情况,内层方法如果出现异常,不能直接回滚,而是对 rollbackOnly 进行标记。外层方法在提交时会调用 isRollbackOnly 方法,如果返回 true,则转入回滚流程,否则继续提交。

public interface SmartTransactionObject {
    boolean isRollbackOnly();
}

JdbcTransactionObjectSupportSmartTransactionObject 接口的抽象子类,提供了一个基于 JDBC 的事务对象。

  • connectionHolder:Spring 封装的数据库连接对象,事务的操作建立在 Connection 的基础之上
  • previousIsolationLevel:存储 Connection 默认的隔离级别。执行事务时可能会指定不同的隔离级别,因此需要暂时替换掉 Connection 默认的隔离级别,等到事务执行完后再恢复。
public abstract class JdbcTransactionObjectSupport implements SmartTransactionObject {
    private ConnectionHolder connectionHolder;
    private Integer previousIsolationLevel;
}

注:实现类 DataSourceTransactionObjectDataSourceTransactionManager 的内部类,有关事务管理器的内容将在下一节介绍。

4. 事务状态

4.1 TransactionStatus

TransactionStatus 接口定义了事务的状态,可以通过编程的方式调用回滚,而不是由于异常引发的回滚。每个事务方法都有对应的事务状态,而这正是独立性的表现,以此为基础方能构成 Spring 事务的横向结构。TransactionStatus 的作用是指导事务方法如何运行,即是否开启事务、回滚或提交事务。

  • isNewTransaction 方法:当前方法的事务是否为新事务。不考虑事务传播的情况下,开启事务的外层方法是新事务,内层方法使用的是原来的事务,所以不是新事务。
  • isCompleted 方法:事务是否已完成,即提交或回滚
public interface TransactionStatus {
    boolean isNewTransaction();
    boolean isCompleted();
}

4.2 AbstractTransactionStatus

AbstractTransactionStatusTransactionStatus 接口的抽象子类,定义了两个标记字段。

  • rollbackOnly 表示局部的回滚标记(我们不关心局部回滚,仅列出来防止混淆)
  • completed 表示事务方法是否执行完毕。在一个事务的生命周期中,提交和回滚都代表事务已完成,要么成功要么失败。
public abstract class AbstractTransactionStatus implements TransactionStatus {
    private boolean rollbackOnly = false;
    private boolean completed = false;

    //是否为全局回滚
    public boolean isGlobalRollbackOnly() {
        return false;
    }
}

isGlobalRollbackOnly 方法表示是否存在全局回滚的情况,子类可重写该方法。也就是说,内层方法抛出异常时不应当立即回滚,而是将全局回滚标记设置为 true。外层方法在提交时检查全局回滚标记,如果为 true 则转入回滚流程。(下文详细介绍全局回滚标记)

4.3 DefaultTransactionStatus

DefaultTransactionStatus 作为默认实现类,定义了很多重要的属性,简单介绍如下:

  • transaction:表示事务对象,可以是任意类型,取决于具体的实现,比如 DataSourceTransactionObject 表示使用 JDBC 数据源实现的事务对象
  • suspendedResources:被挂起的资源,通常是 ConnectionHolder 对象。在已存在事务的情况下,有些业务方法不希望使用事务,或者需要开启新的事务,因此需要将原事务挂起,等到该方法执行完毕后再恢复为原先的事务。
  • newTransaction:表示是否是一个新事务。一般来说,外层方法是新事务,内层方法不是新事务。只有外层方法才会真正执行提交或回滚操作,内层方法的只能通过全局回滚标记传递信号。
  • newSynchronization:表示是否是一个新的同步,与 newTransaction 字段类似,具体的情况具体分析。
  • readOnly:是否为只读事务,比如查询操作是只读的。
public class DefaultTransactionStatus extends AbstractTransactionStatus {
    private final Object transaction
    private final Object suspendedResources;
    private final boolean newTransaction;
    private final boolean newSynchronization;
    private final boolean readOnly;

    @Override
    public boolean isGlobalRollbackOnly() {
        return ((this.transaction instanceof SmartTransactionObject) && ((SmartTransactionObject) this.transaction).isRollbackOnly());
    }
}

此外,DefaultTransactionStatus 还重写了父类的 isGlobalRollbackOnly 方法,可以看到返回的是 SmartTransactionObject 接口的 isRollbackOnly 方法,也就是全局的回滚标记。

5. 事务方法间的通信

5.1 概述

当我们将传统事务的纵向结构改成横向结构之后,出现了一个亟待解决的问题。举例说明,假设 A 和 B 两个方法属于同一个事务,当 B 方法报错时该如何处理?在传统事务中,不论是 A 方法还是 B 方法报错,最终都会被 catch 块捕获,然后回滚事务。但是在 Spring 事务中,每个事务方法都是独立的,B 方法不能直接提交的,而是要以某种方式发出信号,最终由 A 方法来处理。

3.2 事务状态结构.png

从结构上来说,TransactionStatus 持有一个事务对象实例,事务对象的父类 JdbcTransactionObject 持有一个 ConnectionHolder 实例。ConnectionHolder 是数据库连接的包装类,对于同一个事务来说,多个方法指向同一个 ConnectionHolder 实例。父类 ResourceHolderSupportrollbackOnly 属性的作用是对回滚进行标记,由于 ConnectionHolder线程绑定的,因此该属性对全局可见。综上所述,由于 ConnectionHolder 本身的唯一性(同一事务内),使得 rollbackOnly 属性可以作为全局回滚的标记。

5.2 代码实现

DataSourceTransactionObjectDataSourceTransactionManager 的内部类,同时也是事务对象的实现类,有关事务管理器的内容将在下一节介绍。我们来看 setRollbackOnlyisRollbackOnly 方法,都是对 ConnectionHolderrollbackOnly 属性进行操作。

private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport {

    public void setRollbackOnly() {
        getConnectionHolder().setRollbackOnly();
    }

    @Override
    public boolean isRollbackOnly() {
        return getConnectionHolder().isRollbackOnly();
    }
}

之前提到,DefaultTransactionStatus 重写了父类的 isGlobalRollbackOnly 方法,实际上调用了事务对象的 isRollbackOnly 方法。从这里可以看到,全局回滚标记就是线程绑定的 ConnectionHolderrollbackOnley 属性。

@Override
public boolean isGlobalRollbackOnly() {
    return ((this.transaction instanceof SmartTransactionObject) &&
            ((SmartTransactionObject) this.transaction).isRollbackOnly());
}

5.3 全局回滚标记

在实际使用中,当内层方法报错时,调用事务对象的 setRollbackOnly 方法将全局回滚标记为 true。当外层方法准备提交时,检查全局回滚标记,如果为 true 说明内层方法抛出异常,外层方法转入回滚流程。这样一来,外层方法可以获知内层方法的执行情况,从而选择相应的事务操作。

3.3全局回滚标记示意图.png

如图所示,每个事务方法都有对应的 TransactionStatus 实例,它们都指向同一个 ConnectionHolder (线程绑定的事务资源)。当内层方法 C 报错,ConnectionHolderrollbackOnly 属性会被设置为 true。接下来是中间层方法 B,提交时发现全局回滚标记为 true,转入回滚流程。由于方法 B 不是最外层,没有实际的动作。最后是外层方法 A,提交时发现全局回滚标记为 true,转入回滚流程,宣告整个事务执行失败。

6. 事务方法模拟

为了说明 Spring 事务的基本工作原理,我们需要一个辅助类 TransactionMethod,作用是将普通方法包装成符合 Spring 事务特征的事务方法,使其拥有独立执行事务操作的能力。该类的字段较多,按照功能划分为四组,分别介绍如下:

  • targetmethodargs 字段为一组,说明将以反射的方式调用目标方法

  • statusdefinition 字段用于描述事务方法的特征

  • dataSource 字段的作用是获取数据库连接,起辅助作用

  • next 字段比较特殊,指向下一个事务方法,这是事务方法横向结构的关键

execute 方法定义了事务方法执行的核心流程,从结构上来看,这是一个典型的环绕通知。先来看被包裹的部分,执行完目标方法之后,尝试执行下一个事务方法。接下来介绍与事务有关的三个方法。

//测试类
public class TransactionMethod {
    private final Object target;
    private final Method method;
    private final Object[] args;
    private final TransactionStatus status;
    private final TransactionDefinition definition;
    private final DataSource dataSource;
    private TransactionMethod next;

    public void execute() throws Exception {
        doBegin();
        try{
            method.invoke(target, args);

            if (next != null) {
                next.execute(); //执行下一个事务方法
            }
        }catch (Exception e) {
            doRollback();
            throw e;
        }
        doCommit();
    }
}

doBegin 方法的作用是尝试开启事务,分为两步。首先开启事务,具体来说从数据源获取新的连接,并绑定到线程上。这一步只有 TransactionStatus 是新事务才能执行,即外层方法才有资格。其次,将 ConnectionHolder 绑定到事务对象上。

private void doBegin() throws SQLException {
    //1. 如果是新事务,则获取数据库连接,并绑定到线程上
    if (definition != null && status.isNewTransaction()) {
        Connection connection = DataSourceUtils.getConnection(dataSource);
        connection.setAutoCommit(false);
        TransactionSynchronizationManager.bindResource(dataSource, new ConnectionHolder(connection));
    }

    //2. 将 ConnectionHolder 绑定到事务对象上
    ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource);
    MyTransactionObject txObject = (MyTransactionObject) ((DefaultTransactionStatus) status).getTransaction();
    txObject.setConnectionHolder(holder);
}

doRollback 方法有两个分支,如果是新事务则执行回滚操作,否则的话只能将全局回滚标记设置为 true。也就是说内层方法如果出错,只能执行第二个分支流程。

private void doRollback() throws SQLException {
    //1) 如果是新事务,执行回滚操作
    if (status.isNewTransaction()) {
        ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        holder.getConnection().rollback();
        TransactionSynchronizationManager.unbindResource(dataSource);
    }
    //2) 内层方法仅标记回滚
    else {
        MyTransactionObject txObject = (MyTransactionObject) ((DefaultTransactionStatus) status).getTransaction();
        txObject.setRollbackOnly();
    }
}

doCommit 方法也有两个分支。如果全局回滚标记为 true,说明内层方法报错,需要转入回滚流程。如果进入另一个分支,说明内层方法没有报错,进一步检查是否为新事务,也就是说如果是外层方法,则执行提交操作。

private void doCommit() throws SQLException {
    //1) 如果全局回滚标记为true,转入rollback流程
    if(((DefaultTransactionStatus) status).isGlobalRollbackOnly()) {
        doRollback();

        if(status.isNewTransaction()) {
            throw new TransactionException("事务被标记为rollback-only,发生回滚");
        }
        return;
    }

    //2) 如果是新事务,执行提交(内层事务的commit操作不处理)
    if(status.isNewTransaction()) {
        ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        holder.getConnection().commit();
    }
}

我们发现,doBegindoRollbackdoCommit 方法的执行情况首先取决于 TransactionStatus,然后才是其他条件,比如是否为新事务,或者检查全局标记等。这一点从侧面说明了 TransactionStatus 的重要性,每个 TransactionStatus 实例对应一个事务方法,并决定如何执行事务操作。

7. 测试

先准备一个测试类 MyTransactionObject,充当简单的事务对象。由于 DataSourceTransactionObject 是事务管理器的内部类,暂时用 MyTransactionObject 来代替。

//测试类,事务对象
public class MyTransactionObject extends JdbcTransactionObjectSupport {

    public void setRollbackOnly() {
        getConnectionHolder().setRollbackOnly();
    }

    @Override
    public boolean isRollbackOnly() {
        return getConnectionHolder().isRollbackOnly();
    }
}

测试方法有两个要点,一是对两个事务方法进行包装。对于 UserDaosave 方法来说,在构建 TransactionMethod 实例时,传入的 isNewTransaction 参数为 true,说明这是一个新事务,可以执行相关的事务操作。与之相比,我们希望 AccountDaosave 方法使用已经存在的事务,因此在构建 TransactionMethod 实例时传入的 isNewTransaction 参数为 false。二是让两个事务方法构成一条事务链,并执行第一个方法。

表面上看,测试代码中没有出现事务的相关操作,所有的细节都被隐藏起来。尽管两个事务方法是分别调用的,但实际上它们属于同一个事务。此外,我们还可以根据业务的需要,增加新的事务方法,而这正是事务方法的灵活性的体现。

//测试方法
@Test
public void testSpringTransaction() throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DataSourceConfig.class);
    DataSource dataSource = context.getBean(DataSource.class);

    //包装UserDao的save方法
    UserDao userDao = context.getBean(UserDao.class);
    Method method = ReflectionUtils.findMethod(UserDao.class, "save", null);
    TransactionMethod userTm = new TransactionMethod(userDao, method, new Object[] {"Stimd", "12307"}, true, dataSource);

    //包装AccountDao的save方法
    AccountDao accountDao = context.getBean(AccountDao.class);
    Method method2 = ReflectionUtils.findMethod(AccountDao.class, "save", null);
    TransactionMethod accountTm = new TransactionMethod(accountDao, method2, new Object[]{"12307"}, false, dataSource);

    //构建事务链,并执行事务方法
    userTm.next(accountTm);
    userTm.execute();
}

从测试结果可以看到,AccountDaosave 方法报错了,但没有执行回滚操作,而是标记全局回滚字段,然后由 UserDaosave 方法执行回滚操作。测试结果符合预期,这说明两个事务方法的事务是分别控制的,并且通过全局标记进行通信。

开启事务:public int tx.common.UserDao.save(java.lang.String,java.lang.String)
仅标记回滚:public void tx.common.AccountDao.save(int)
回滚事务:public int tx.common.UserDao.save(java.lang.String,java.lang.String)
Caused by: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '12307' for key 't_account.idx_unique_phone'

8. 总结

本节介绍了 Spring 事务与传统事务的区别,传统事务实际上是纵向结构,同一个事务中的方法被包裹在内部,由外层方法统一进行事务管理。这种方式优点是编写代码简单,缺点是不够灵活,比如想开启新事务很难处理。Spring 事务是横向结构,每个事务方法都是独立的,可以自行决定是否开启新事务、回滚或提交事务。这样一来,灵活性大大增强,代价则是组织结构变得更加复杂,需要更多的类来描述。

首先,Spring 事务需要通过三个组件进行描述,简单介绍如下:

  • TransactionDefinition:描述了事务方法的属性,即决定了一个方法是否有资格被认定为事务方法

  • TransactionStatus:每个事务方法都有对应的事务状态,用于指导事务方法如何运行

  • SmartTransactionObject:表示一个事务对象,持有一个 Connection 实例

3.4 Spring事务脑图.png

其次,Spring 事务的每个方法都可以自行决定是否开启、提交或回滚事务,这是独立性的体现。但对于同一事务下的方法来说,有必要保持一定的联系。由于每个事务方法对应一个 TransactionStatus,它们指向同一个 ConnectionHolder 实例,因此可以使用 rollbackOnly 字段作为全局回滚标记。一旦内层方法报错,外层方法就会根据标记来回滚事务。

最后,需要将独立的事务方法串联起来,以执行链的方式依次调用。Spring 是通过事务管理器来完成的,这部分内容较多,我们将在下一节详细介绍。本节通过自定义的 TransactionMethod 模拟了 Spring 事务的执行流程,综合使用了上述的所有知识点。

9. 项目信息

新增修改一览,新增(9),修改(1)。

tx
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring
   │        ├─ jdbc
   │        │  └─ datasource
   │        │     └─ JdbcTransactionObjectSupport.java (+)
   │        └─ transaction
   │           ├─ support
   │           │  ├─ AbstractTransactionStatus.java (+)
   │           │  ├─ DefaultTransactionDefinition.java (+)
   │           │  ├─ DefaultTransactionStatus.java (+)
   │           │  ├─ SmartTransactionObject.java (+)
   │           ├─ TransactionDefinition.java (+)
   │           └─ TransactionStatus.java (+)
   └─ test
      └─ java
         └─ tx
            └─ transaction
               ├─ MyTransactionObject.java (+)
               ├─ TransactionMethod.java (+)
               └─ TransactionTest.java (*)

注:+号表示新增、*表示修改

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


欢迎关注公众号【Java编程探微】,加群一起讨论。

原创不易,觉得内容不错请关注、点赞、收藏。