1. 前言
上一节介绍了 Spring 事务的基本原理,引入横向结构保证了事务方法的独立性,又通过全局回滚标记使得事务方法之间可以通信。现在的问题是,Spring 事务是如何运行的?具体来说,Spring 提供了事务管理器 来对事务进行管理。开发者可以通过该组件来编写事务逻辑,这种方式也称为编程式事务。
2. 事务管理器
2.1 概述
事务管理器作为 tx 模块的核心组件,不仅提供了控制事务的底层逻辑,还涉及到事务的传播行为。继承结构比较简单,我们主要关心本地事务的处理。
PlatformTransactionManager
:顶级接口,定义了事务的操作AbstractPlatformTransactionManager
:实现了开启事务、提交事务和回滚事务的主体流程,细节部分由子类完成DataSourceTransactionManager
:基于 JDBC 的事务管理器,可以处理本地事务JtaTransactionManager
:负责实现全局事务(仅了解)
2.2 PlatformTransactionManager
PlatformTransactionManager
接口定义了操作事务的行为,这三个方法需要相互配合才能实现完成的事务操作。
getTransaction
:获取事务状态,也就是开启事务commit
:提交事务rollback
:回滚事务
java
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
2.3 AbstractPlatformTransactionManager
AbstractPlatformTransactionManager
作为抽象实现类,通过模板方法模式 实现了父接口的三个方法。也就是说,该类只完成了开启事务、提交和回滚事务的基本流程,细节部分由子类来实现 doGetTransaction
等抽象方法。
java
public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager {
//重写父类的具体操作
@Override
public TransactionStatus getTransaction(TransactionDefinition definition) {}
@Override
public void commit(TransactionStatus status) {}
@Override
public void rollback(TransactionStatus status) {}
//抽象模板方法,交给子类的事务对象来处理
protected abstract Object doGetTransaction();
protected abstract boolean isExistingTransaction(Object transaction);
protected abstract void doBegin(Object transaction, TransactionDefinition definition);
protected abstract Object doSuspend(Object transaction);
protected abstract void doResume(Object transaction, Object suspendResources);
protected abstract void doCommit(DefaultTransactionStatus status);
protected abstract void doRollback(DefaultTransactionStatus status);
protected abstract void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException;
protected abstract void doCleanupAfterCompletion(Object transaction);
}
2.4 DataSourceTransactionManager
DataSourceTransactionManager
作为默认的实现类,针对 JDBC 这一特殊场景的具体实现。该类持有一个 dataSource
字段,表示数据源。一个应用中可以存在多个事务管理器,一个事务管理器对应一个数据源。
java
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager {
private DataSource dataSource;
public DataSourceTransactionManager(DataSource dataSource) {
this.dataSource = dataSource;
}
//内部类,事务对象
private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport {
public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
@Override
public boolean isRollbackOnly() {
return getConnectionHolder().isRollbackOnly();
}
}
}
此外,内部类 DataSourceTransactionObject
是事务对象 的具体实现,setRollbackOnly
和 isRollbackOnly
方法说明对 rollbackOnly 的访问是全局性的。关于这一点已经在上节介绍过了。
3. 事务管理
3.1 获取事务
接下来,我们以 AbstractPlatformTransactionManager
为切入点,详细分析事务操作的相关方法,首先来看获取事务的流程。getTransaction
方法涉及到事务的传播行为,为了降低代码的复杂度,我们先讨论最简单的情况,即多个方法属于同一个事务。创建新事务的过程可以分为三步,首先获取事务对象,然后执行开启事务的流程,最后创建 DefaultTransactionStatus
对象并返回。
java
//所属类[cn.stimd.spring.transaction.support.AbstractPlatformTransactionManager]
@Override
public TransactionStatus getTransaction(TransactionDefinition definition) {
//TODO 涉及事务的传播,先简化处理,后续完善
//1. 获取事务对象
Object transaction = doGetTransaction();
//2. 开启事务
doBegin(transaction, definition);
//3. 返回事务状态
DefaultTransactionStatus status = new DefaultTransactionStatus(transaction, null, true, true, definition.isReadOnly());
prepareSynchronization(status, definition);
return status;
}
第一步,获取事务对象。doGetTransaction
是模板方法,由子类 DataSourceTransactionManager
实现。首先创建事务对象的实例,然后尝试获取线程绑定的 Connection,并绑定到事务对象上。第一次执行 doGetTransaction
方法,线程上没有绑定 ConnectionHolder
,这时的事务对象实际上是一个空事务。
java
//所属类[cn.stimd.spring.jdbc.datasource.DataSourceTransactionManager]
@Override
protected Object doGetTransaction() {
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
//尝试获取线程绑定的Connection,第一次为null
ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource);
txObject.setConnectionHolder(holder);
return txObject;
}
第二步,开启事务。DataSourceTransactionManager
实现了 doBegin
方法,可以分为三步:
- 获取数据库连接。事务对象上可能没有绑定 Connection,需要从数据源中拿到 Connection,并设置到事务对象上。
- 对数据库连接和事务对象的一些设置,包括设置隔离级别,关闭自动提交,设置事务开启的标记,设置超时时间等。
- 将
ConnectionHolder
绑定到线程上,此时的事务对象才是真正可用的。
java
//所属类[cn.stimd.spring.jdbc.datasource.DataSourceTransactionManager]
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
//1. 获取数据库连接,并绑定到事务对象上
if(txObject.getConnectionHolder() == null){
Connection newCon = this.dataSource.getConnection();
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
con = txObject.getConnectionHolder().getConnection();
//2. 准备工作
//设置隔离级别
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
//关闭自动提交
if(con.getAutoCommit()){
con.setAutoCommit(false);
}
//设置事务开启标记,判定是否事务存在的依据
txObject.getConnectionHolder().setTransactionActive(true);
//设置超时
if(definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT){
txObject.getConnectionHolder().setTimeoutInSeconds(definition.getTimeout());
}
//3. 将Connection绑定到线程上
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(this.dataSource, txObject.getConnectionHolder());
}
}catch (Throwable e){
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, this.dataSource);
txObject.setConnectionHolder(null, false);
}
throw new TransactionException("Could not open JDBC Connection for transaction", ex);
}
}
3.2 提交事务
提交事务的复杂之处在于不仅仅只有提交的工作,某些情况下需要进行回滚。在多方法的事务中,假如内层的事务方法出现异常,外层的事务方法正常执行。那么外层的事务方法在执行 commit
方法时,需要对整个事务进行回滚。原因在于,为了保证事务的一致性,内层方法不能直接回滚,只能由外层方法进行统一处理。理解了这一点,我们来看提交操作的具体流程,可以分为三步:
- 如果事务已完成(提交或回滚),不能重复执行,抛出异常。
- 检查全局回滚标记,如果为 true,则进入回滚的流程。对于外层的事务方法,最后还需要抛出异常,宣告整个事务执行失败。
- 如果不需要回滚,调用
processCommit
方法进一步执行提交流程。
java
//所属类[cn.stimd.spring.transaction.support.AbstractPlatformTransactionManager]
@Override
public void commit(TransactionStatus status) {
//1. 检查事务是否已完成,避免重复执行
if (status.isCompleted()) {
throw new IllegalStateException("提交失败,事务已提交或回滚");
}
//2. 如果rollbackOnly被标记为true,说明内层事务发生异常,需要回滚
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if(defStatus.isGlobalRollbackOnly()){
//转向回滚流程
processRollback(defStatus);
//如果是新事务,即最外层事务,需要抛出异常,宣告整个事务执行失败
if(status.isNewTransaction()){
throw new TransactionException("事务被标记为rollback-only,发生回滚");
}
return;
}
//3. 执行提交流程
processCommit(defStatus);
}
在 processCommit
方法中,需要考虑两种情况。一是检查是否为新事务,也就是说由开启事务的方法来提交事务,确保该方法和之后调用的方法在一个事务里。DataSourceTransactionManager
实现了真正的提交操作,在 doCommit
方法中,先拿到事务对象,进一步获取数据库连接,然后执行数据库连接的 commit
方法即可。
java
//所属类[cn.stimd.spring.transaction.support.AbstractPlatformTransactionManager]
private void processCommit(DefaultTransactionStatus status) {
try {
try {
//只有新事务,才执行提交操作
if(status.isNewTransaction()){
doCommit(status);
}
} catch (RuntimeException | Error ex) {
//出现异常,触发回滚
doRollbackOnCommitException(status);
throw ex;
}
} finally {
//清理操作
cleanupAfterCompletion(status);
}
}
//所属类[cn.stimd.spring.jdbc.datasource.DataSourceTransactionManager]
@Override
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
try {
con.commit();
} catch (SQLException e) {
throw new TransactionException("提交事务失败");
}
}
此外,提交过程中可能出现异常,需要转入回滚流程。doRollbackOnCommitException
方法处理了两种情况,内层的事务方法仅标记全局回滚,外层事务方法才执行真正的回滚操作。
java
//所属类[cn.stimd.spring.transaction.support.AbstractPlatformTransactionManager]
//回滚在提交时发生的异常
private void doRollbackOnCommitException(DefaultTransactionStatus status) throws TransactionException {
//新事务,执行回滚操作
if (status.isNewTransaction()) {
doRollback(status);
}
//内层事务,将rollbackOnly标记设置为true
else if (status.hasTransaction()) {
doSetRollbackOnly(status);
}
}
3.3 回滚事务
先来看 rollback
方法。首先检查事务的状态是否标记为已完成,如果是则说明事务已提交或关闭,不能重复执行,抛出异常。接下来进入 processRollback
方法,对事务的处理可以分为两种情况:
- 如果是新事务,执行真正的回滚操作
- 不是新事务,且事务对象存在,说明是内层事务,只需要将全局回滚标记设置为
true
。
java
//所属类[cn.stimd.spring.transaction.support.AbstractPlatformTransactionManager]
@Override
public void rollback(TransactionStatus status) {
if (status.isCompleted()) {
throw new IllegalStateException("回滚失败,事务已提交或回滚");
}
processRollback((DefaultTransactionStatus) status);
}
//所属类[cn.stimd.spring.transaction.support.AbstractPlatformTransactionManager]
private void processRollback(DefaultTransactionStatus status) {
try {
//1. 只有新事务,才执行真正的回滚操作
if (status.isNewTransaction()) {
doRollback(status);
}
//2. 内层事务,将rollbackOnly标记设置为true
else if(status.hasTransaction()){
doSetRollbackOnly(status);
}
} finally {
//清理操作
cleanupAfterCompletion(status);
}
}
DataSourceTransactionManager
实现了真正的回滚操作,doRollback
方法的实现比较简单,先获取事务对象,然后获取数据库连接对象,最后调用数据库连接的 rollback
方法即可。
java
//所属类[cn.stimd.spring.jdbc.datasource.DataSourceTransactionManager]
@Override
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
try {
con.rollback();
} catch (SQLException e) {
throw new TransactionException("回滚事务失败");
}
}
3.4 事务完成的清理工作
提交和回滚都被视为事务完成,都需要进行收尾工作,因此都会调用 cleanupAfterCompletion
方法。事务的清理工作可以分为四步,如下所示:
- 首先将
completed
标记为true
,避免重复执行提交或回滚操作。 - 如果是新的同步,清除线程绑定的相关资源(仅了解即可)
- 如果是新的事务,则由
doCleanupAfterCompletion
方法来处理 - 恢复先前挂起的事务。(待实现)
注:挂起(suspend)和恢复(resume)是一组操作,彼此不可分割。前边提到,开启事务是一个非常复杂的操作,目前只讨论最简单的情况,因此省略了挂起的操作。同样地,暂时略过
resume
方法的实现。具体细节将在「事务的传播行为」一节中介绍。
java
//所属类[cn.stimd.spring.transaction.support.AbstractPlatformTransactionManager]
private void cleanupAfterCompletion(DefaultTransactionStatus status) {
//将completed属性设置为true
status.setCompleted();
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.clear();
}
if (status.isNewTransaction()) {
doCleanupAfterCompletion(status.getTransaction());
}
if (status.getSuspendedResources() != null) {
resume(status.getTransaction(), (SuspendedResourcesHolder) status.getSuspendedResources());
}
}
接下来我们来看 doCleanupAfterCompletion
方法的具体实现,主要由四个步骤组成:
- 解除线程绑定的数据源,这里的资源实际上就是
ConnectionHolder
。 - 恢复当前 Connection 上原先的隔离级别和只读属性,以便重新使用。
- 释放数据库连接。需要注意的是,对于内层的事务方法来说,并不会真正地释放连接。二是,一般来说
DataSource
使用的是连接池,因此释放的连接并不会真正关闭,而是放回池中,提高使用效率。 - 重置事务对象的
ConnectionHolder
,具体来说transactionActive
属性设置为false
,说明该数据库连接与事务无关。
java
//所属类[cn.stimd.spring.jdbc.datasource.DataSourceTransactionManager]
@Override
protected void doCleanupAfterCompletion(Object transaction) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
//解除线程绑定
TransactionSynchronizationManager.unbindResource(this.dataSource);
//重置Connection隔离级别和只读属性
Connection con = txObject.getConnectionHolder().getConnection();
DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
//释放数据库连接
DataSourceUtils.releaseConnection(con, this.dataSource);
//重置ConnectionHolder
txObject.getConnectionHolder().clear();
}
4. 事务模板
4.1 TransactionOperations
由于事务管理器的相关操作是固定的,Spring 提供了模板操作来规范这一过程,简化了事务代码的执行逻辑。TransactionOperations
接口定义了事务模板的行为,execute
方法的作用是执行事务代码,泛型参数 T 代表返回结果。TransactionCallback
接口的作用是以回调的方式执行业务代码,用户需要自行提供实现类。
java
public interface TransactionOperations {
<T> T execute(TransactionCallback<T> action) throws TransactionException;
}
public interface TransactionCallback<T> {
T doInTransaction(TransactionStatus status);
}
4.2 TransactionTemplate
TransactionTemplate
扩展了 DefaultTransactionDefinition
,持有一个事务管理器实例。execute
方法的作用是在事务中执行业务代码,可以分为四步。一是开启事务,二是执行业务代码,三是出现异常时回滚,四是提交事务。回滚事务分三种情况,统一调用 rollbackOnException
方法处理。
java
public class TransactionTemplate extends DefaultTransactionDefinition implements TransactionOperations{
private PlatformTransactionManager transactionManager;
@Override
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
//1) 开启事务
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
//2) 执行业务代码回调
result = action.doInTransaction(status);
}
//3) 出现异常回滚事务
//业务异常
catch (RuntimeException ex) {
rollbackOnException(status, ex);
throw ex;
}
//错误
catch (Error err) {
rollbackOnException(status, err);
throw err;
}
//未知异常
catch (Throwable ex) {
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
//4) 提交事务
this.transactionManager.commit(status);
return result;
}
//出现异常回滚
private void rollbackOnException(TransactionStatus status, Throwable ex) throws TransactionException {
try {
this.transactionManager.rollback(status);
} catch (RuntimeException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
} catch (Error err) {
logger.error("Application exception overridden by rollback error", ex);
throw err;
}
}
}
5. 事务处理流程
5.1 概述
无论是手动调用事务管理器,还是通过事务模板类来完成,都需要 PlatformTransactionManager
接口的三个方法进行配合。单一方法的事务比较简单,出现异常就回滚,正常执行则提交。对于多方法的事务来说,我们不知道哪个方法会出现异常,因此由谁来执行回滚或提交操作是首要考虑的问题 。针对不同的情况,commit
和 rollback
方法会执行不同的分支。我们对多个事务方法之间可能出现的情况进行梳理,列举了以下六种情况,结合流程图来简单分析:
-
1A-2A:当前方法报错,且是新事务,可以看做是外层方法,直接回滚。
-
1A-2B:当前方法报错,但不是新事务,说明是内层方法,仅标记为全局回滚,需要外层方法进一步处理。
-
1B-3A-2A:当前方法正常执行,但检测到全局回滚标记为 true,说明内层方法报错了。且是新事务,说明当前方法是外层方法,仍需要回滚。(提交转回滚流程)
-
1B-3A-2B:当前方法正常执行,但检测到全局回滚标记为 true,说明内层方法报错了。但不是新事务,说明当前方法也是内层方法,因此仅标记全局回滚。(与第二种情况类似)
-
1B-3B-4A:当前方法正常执行,全局回滚标记为 false(可能不存在内层方法,也可能内层方法没报错)。且是新事务,说明当前方法是外层方法,执行提交流程。
-
1B-3B-4B:当前方法正常执行,全局回滚标记为 false。但不是新事务,说明当前方法是内层方法,不做任何处理,最后由外层方法进行提交。
综上所述,在不考虑传播行为的情况下,即假设多个方法属于同一个事务,那么我们可以得出一个总的原则:内层方法不执行实际的提交和回滚操作,内层方法出现异常时仅标记全局回滚,外层方法负责开启事务、提交和回滚的操作。
5.2 应用示例
上述六种基本情况是针对单个事务方法而言的,推而广之,我们可以组合出多个事务方法可能出现的所有情况。由于流程图仅代表一个方法的执行流程,对于多个方法来说,会重复执行整个事务流程。假设 A 和 B 两个方法属于同一事务,B 方法是内层方法,先于 A 方法执行完毕。因此执行轨迹先是 B 方法的,然后是 A 方法。为了区别两个方法,这里使用中括号进行分隔。
-
B 方法报错,A 方法正常执行:[1A-2B]-[1B-3A-2A]
-
B 方法和 A 方法均正常执行:[1B-3B-4B]-[1B-3B-4A]
-
B 方法正常执行,A 方法报错:[1B-3B-4B]-[1A-2A]
-
B 方法和 A 方法均报错:[1A-2B]-[1A-2A]
注:示例并没有涵盖所有的六种情况,原因在于第三和第六种情况需要至少三个方法,感兴趣的读者可以自行推断。
6. 测试
6.1 事务管理器
先来看配置类 TransactionConfig
有两个作用,一是导入 DataSourceConfig
配置类,加载数据源;二是注册事务管理器。
java
//测试类
@Configuration
@Import(DataSourceConfig.class)
public class TransactionConfig {
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
在测试方法中,先启动 Spring 容器,获取事务管理器组件。然后获取 TransactionStatus
实例,此时事务已经开启。接下来是业务方法,包裹在 try...catch 块中。如果出现异常,调用事务管理器的 rollback
方法进行回滚。如果正常执行,最后调用事务管理器的 commit
方法提交事务。
java
//测试方法
@Test
public void testTransactionManager() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TransactionConfig.class);
DataSourceTransactionManager txManager = context.getBean(DataSourceTransactionManager.class);
//开启事务
TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
try{
UserService userService = context.getBean(UserService.class);
userService.register("Stimd", "12307");
}catch (Exception ex) {
//回滚事务,抛出异常
txManager.rollback(status);
throw ex;
}
//提交事务
txManager.commit(status);
}
从测试结果可以看到,先开启事务,然后业务方法报错触发回滚,并打印异常堆栈的信息。再观察数据表,并没有新增一条记录,说明事务生效了。
css
[Tx] [事务管理] --> 事务不存在,开启新事务
[Tx] [事务管理] --> 回滚事务
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '12307' for key 't_account.idx_unique_phone'
6.2 事务模板
在测试方法中,先是准备工作,然后创建 TransactionTemplate
实例,并调用 exeucte
方法。入参是一个匿名的 TransactionCallback
实现类,在回调方法 doInTransaction
中执行业务代码。测试结果与上一个测试的相同,可以看到事务模板的代码更简洁,所有事务相关的操作都被隐藏了。
java
//测试方法
@Test
public void testTransactionTemplate() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TransactionConfig.class);
DataSourceTransactionManager txManager = context.getBean(DataSourceTransactionManager.class);
//创建模板类
TransactionTemplate template = new TransactionTemplate(txManager);
template.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
UserService userService = context.getBean(UserService.class);
userService.register("Stimd", "12307");
return null;
}
});
}
7. 总结
本节介绍了 Spring 事务的实现流程,即通过事务管理器来控制事务的开启、提交和回滚。这三个方法需要互相配合,才能实现一个完整的事务管理。为此,着重介绍了事务处理流程中可能出现的几种情况。此外,事务管理器的相关操作是固定的,Spring 提供模板类 TransactionTemplate
简化了这一操作。
总的来说,事务管理器和事务模板仍存在不足,主要有两点。一是它们针对的都是单一的事务方法,不能处理多个事务方法的调用,以及更为复杂的事务传播。二是事务操作广泛地出现在应用程序中,使用编程式事务并不是一个明智的选择,我们需要更为便捷的处理方式,即声明式的事务切面。
8. 项目信息
新增修改一览,新增(8),修改(2)。
scss
tx
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring
│ ├─ jdbc
│ │ └─ datasource
│ │ ├─ DataSourceTransactionManager.java (+)
│ │ └─ DataSourceUtils.java (*)
│ └─ transaction
│ ├─ support
│ │ ├─ AbstractPlatformTransactionManager.java (+)
│ │ ├─ TransactionCallback.java (+)
│ │ ├─ TransactionOperations.java (+)
│ │ ├─ TransactionSynchronizationUtils.java (+)
│ │ └─ TransactionTemplate.java (+)
│ └─ PlatformTransactionManager.java (+)
└─ test
└─ java
└─ tx
└─ transaction
├─ TransactionConfig.java (+)
└─ TransactionTest.java (*)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。