1. 前言
在传统的事务结构中,外层的方法控制 Connection 执行相关的事务操作,内层的方法完成具体的 JDBC 操作。如下图所示,绿色区域代表外层方法,内层方法 step1 和 step2 都置于同一个事务当中。在执行内层方法前必定开启事务,在内层方法执行完后提交或回滚事务,这些步骤都是固定的。默认情况下,内层的方法都属于同一个事务,如果某个方法想脱离当前事务,或者开启新的事务,处理起来会比较麻烦。
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:是否为只读事务,默认 falsename:当前方法的事务名称
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();
}
JdbcTransactionObjectSupport 是 SmartTransactionObject 接口的抽象子类,提供了一个基于 JDBC 的事务对象。
connectionHolder:Spring 封装的数据库连接对象,事务的操作建立在 Connection 的基础之上previousIsolationLevel:存储 Connection 默认的隔离级别。执行事务时可能会指定不同的隔离级别,因此需要暂时替换掉 Connection 默认的隔离级别,等到事务执行完后再恢复。
public abstract class JdbcTransactionObjectSupport implements SmartTransactionObject {
private ConnectionHolder connectionHolder;
private Integer previousIsolationLevel;
}
注:实现类
DataSourceTransactionObject是DataSourceTransactionManager的内部类,有关事务管理器的内容将在下一节介绍。
4. 事务状态
4.1 TransactionStatus
TransactionStatus 接口定义了事务的状态,可以通过编程的方式调用回滚,而不是由于异常引发的回滚。每个事务方法都有对应的事务状态,而这正是独立性的表现,以此为基础方能构成 Spring 事务的横向结构。TransactionStatus 的作用是指导事务方法如何运行,即是否开启事务、回滚或提交事务。
isNewTransaction方法:当前方法的事务是否为新事务。不考虑事务传播的情况下,开启事务的外层方法是新事务,内层方法使用的是原来的事务,所以不是新事务。isCompleted方法:事务是否已完成,即提交或回滚
public interface TransactionStatus {
boolean isNewTransaction();
boolean isCompleted();
}
4.2 AbstractTransactionStatus
AbstractTransactionStatus 是 TransactionStatus 接口的抽象子类,定义了两个标记字段。
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 方法来处理。
从结构上来说,TransactionStatus 持有一个事务对象实例,事务对象的父类 JdbcTransactionObject 持有一个 ConnectionHolder 实例。ConnectionHolder 是数据库连接的包装类,对于同一个事务来说,多个方法指向同一个 ConnectionHolder 实例。父类 ResourceHolderSupport 的 rollbackOnly 属性的作用是对回滚进行标记,由于 ConnectionHolder 是线程绑定的,因此该属性对全局可见。综上所述,由于 ConnectionHolder 本身的唯一性(同一事务内),使得 rollbackOnly 属性可以作为全局回滚的标记。
5.2 代码实现
DataSourceTransactionObject 是 DataSourceTransactionManager 的内部类,同时也是事务对象的实现类,有关事务管理器的内容将在下一节介绍。我们来看 setRollbackOnly 和 isRollbackOnly 方法,都是对 ConnectionHolder 的 rollbackOnly 属性进行操作。
private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport {
public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
@Override
public boolean isRollbackOnly() {
return getConnectionHolder().isRollbackOnly();
}
}
之前提到,DefaultTransactionStatus 重写了父类的 isGlobalRollbackOnly 方法,实际上调用了事务对象的 isRollbackOnly 方法。从这里可以看到,全局回滚标记就是线程绑定的 ConnectionHolder 的 rollbackOnley 属性。
@Override
public boolean isGlobalRollbackOnly() {
return ((this.transaction instanceof SmartTransactionObject) &&
((SmartTransactionObject) this.transaction).isRollbackOnly());
}
5.3 全局回滚标记
在实际使用中,当内层方法报错时,调用事务对象的 setRollbackOnly 方法将全局回滚标记为 true。当外层方法准备提交时,检查全局回滚标记,如果为 true 说明内层方法抛出异常,外层方法转入回滚流程。这样一来,外层方法可以获知内层方法的执行情况,从而选择相应的事务操作。
如图所示,每个事务方法都有对应的 TransactionStatus 实例,它们都指向同一个 ConnectionHolder (线程绑定的事务资源)。当内层方法 C 报错,ConnectionHolder 的 rollbackOnly 属性会被设置为 true。接下来是中间层方法 B,提交时发现全局回滚标记为 true,转入回滚流程。由于方法 B 不是最外层,没有实际的动作。最后是外层方法 A,提交时发现全局回滚标记为 true,转入回滚流程,宣告整个事务执行失败。
6. 事务方法模拟
为了说明 Spring 事务的基本工作原理,我们需要一个辅助类 TransactionMethod,作用是将普通方法包装成符合 Spring 事务特征的事务方法,使其拥有独立执行事务操作的能力。该类的字段较多,按照功能划分为四组,分别介绍如下:
-
target、method和args字段为一组,说明将以反射的方式调用目标方法 -
status和definition字段用于描述事务方法的特征 -
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();
}
}
我们发现,doBegin、doRollback 和 doCommit 方法的执行情况首先取决于 TransactionStatus,然后才是其他条件,比如是否为新事务,或者检查全局标记等。这一点从侧面说明了 TransactionStatus 的重要性,每个 TransactionStatus 实例对应一个事务方法,并决定如何执行事务操作。
7. 测试
先准备一个测试类 MyTransactionObject,充当简单的事务对象。由于 DataSourceTransactionObject 是事务管理器的内部类,暂时用 MyTransactionObject 来代替。
//测试类,事务对象
public class MyTransactionObject extends JdbcTransactionObjectSupport {
public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
@Override
public boolean isRollbackOnly() {
return getConnectionHolder().isRollbackOnly();
}
}
测试方法有两个要点,一是对两个事务方法进行包装。对于 UserDao 的 save 方法来说,在构建 TransactionMethod 实例时,传入的 isNewTransaction 参数为 true,说明这是一个新事务,可以执行相关的事务操作。与之相比,我们希望 AccountDao 的 save 方法使用已经存在的事务,因此在构建 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();
}
从测试结果可以看到,AccountDao 的 save 方法报错了,但没有执行回滚操作,而是标记全局回滚字段,然后由 UserDao 的 save 方法执行回滚操作。测试结果符合预期,这说明两个事务方法的事务是分别控制的,并且通过全局标记进行通信。
开启事务: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 实例
其次,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编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。