理解 Spring 提供的数据库事务管理

562 阅读11分钟

多亏了 Spring 提供的事务管理功能,让我们可以专注于实现业务逻辑,而无需过多的关注事务的开始、提交、回滚、传播等。

今天,我们就来一起看下 Spring 都帮我们做了哪些事情,以及,都是如何做到的。

Spring 事务配置

我们先来看一下 Spring 帮我们来管理事务用到了哪些核心类,以及他们都是做什么的。

事务管理器:PlatformTransactionManager

Spring 事务管理中的核心类。

根据设计经验,越是核心的东西,就应该越简单。所以,该接口也仅仅提供了三个方法。

  1. 根据当前的事务传播机制,返回现有活跃事务或创建一个新事务。
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException;
  1. 根据事务状态,提交指定的事物。
void commit(TransactionStatus status) throws TransactionException;
  1. 对指定的事物进行回滚。
void rollback(TransactionStatus status) throws TransactionException;

编程式事务入口:TransactionTemplate

和 Spring 为我们提供的其他 xxxTemplate 一样,TransactionTemplate 就是我们使用编程式事务管理的入口类。

在该类中,以模板方法的形式,给我们提供了执行事务的入口:

public <T> T execute(TransactionCallback<T> action) throws TransactionException 

声明式事务管理:TransactionAspectSupport

@Transactional 注解对应,采用 AOP 的方式管理事务的生命周期。

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

事务属性 TransactionDefinition

传播机制 PROPAGATION

传播机制描述
PROPAGATION_REQUIRED支持当前事务,如果当前没有事务的话,就新建一个。
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务的话,就不在事务中执行。
PROPAGATION_MANDATORY支持当前事务,如果当前没有事务的话,就抛出异常。
PROPAGATION_REQUIRES_NEW创建一个新的事物,如果当前有事务的话,就将当前事务挂起。
PROPAGATION_NOT_SUPPORTED不支持当前事务,总在无事务中执行。
PROPAGATION_NEVER不支持事务,如果当前有事务的话,就抛异常。
PROPAGATION_NESTED如果当前有事务的话,作为其嵌套事务执行。

隔离级别 ISOLATION

隔离级别描述
ISOLATION_READ_UNCOMMITTED可能发生脏读,不可重复读,幻读。
ISOLATION_READ_COMMITTED可以避免脏读,可能发生不可重复读,幻读。
ISOLATION_REPEATABLE_READ可以避免脏读,不可重复读,可能发生幻读。
ISOLATION_SERIALIZABLE可以避免脏读,不可重复读,幻读。

超时时间 TIMEOUT

只读 read-only

名称 name

事务执行流程

这里以编程式事务过例,过一下在 Spring 中,一个事务从开启,到结束的执行流程。

执行入口 execute

Spring 的编程式事务管理能力是由 TransactionTemplate 提供的。 执行事务的入口为 execute 方法, 调用方使用结构大致如下:

transactionTemplate.execute((TransactionCallback) transactionStatus -> {
     try {
          //事务
     }catch (Exception e){
          transactionStatus.setRollbackOnly();
     }
});

下面,先进入到入口 execute 方法看一下具体实现:

public <T> T execute(TransactionCallback<T> action) throws TransactionException {
        ...
      TransactionStatus status = this.transactionManager.getTransaction(this);
      try{
	  result = action.doInTransaction(status);

      }catch (...){
          rollbackOnException(status, ex);
          ...
      }
      ...
      this.transactionManager.commit(status);
      return result;

}

从上面的代码中可以清晰的看出整个事务的执行流程:获取 -> 执行 -> 异常回滚/提交

获取事务 getTransaction

* Return a currently active transaction or create a new one, according to
* the specified propagation behavior.

执行事务的第一步:使用 transactionManager 调用 getTransaction 获取事务,入参即为上面介绍过的事务属性 TransactionDefinition

我把该方法代码简化后如下:

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
	Object transaction = doGetTransaction();
        
	if (isExistingTransaction(transaction)) {
            return handleExistingTransaction(definition, transaction, debugEnabled);
	}
        if ( PROPAGATION_MANDATORY ){
            throw ...
        }else if (PROPAGATION_REQUIRED || PROPAGATION_REQUIRES_NEW || PROPAGATION_NESTED ){
            try{
                SuspendedResourcesHolder suspendedResources = suspend(null);
                DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                doBegin(transaction, definition);
                prepareSynchronization(status, definition);
                return status;
            }catch( ... ){
                resume(null, suspendedResources);
		throw ex;
            }
        }
        return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);

创建事务对象 doGetTransaction

* Return a transaction object for the current transaction state.

该方法内逻辑很简单:

  1. 简单的创建出 DataSourceTransactionObject 对象。
  2. 尝试以当前数据源为 Key ,从 ThreadLocal 中取出当前事务绑定的连接(如果有的话)设置进事务属性中。

当前 ThreadLocal 内没有被放入连接,则说明目前不在事务中。

判断当前是否已有事务 isExistingTransaction

* Check if the given transaction object indicates an existing transaction
* (that is, a transaction which has already started).

事务对象中保存有数据库连接,且对应的事务为活跃状态。

根据传播机制处理已存在事务 handleExistingTransaction

* Create a TransactionStatus for an existing transaction.

PROPAGATION_NEVER

根据事务传播机制定义,在该分支内,会直接抛异常。

throw new IllegalTransactionStateException(
					"Existing transaction found for transaction marked with propagation 'never'");

PROPAGATION_NOT_SUPPORTED

需要在无事务中执行。所以,在该分支内会将已有事务挂起。

Object suspendedResources = suspend(transaction);

PROPAGATION_REQUIRES_NEW

创建新事务,并将当前事务挂起。

SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    DefaultTransactionStatus status = newTransactionStatus(
						definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    doBegin(transaction, definition);
    prepareSynchronization(status, definition);
    return status;
}catch ( ... ) {
    resumeAfterBeginException(transaction, suspendedResources, beginEx);
    throw beginEx;
}

PROPAGATION_NESTED

嵌套事务执行。创建并存储检查点。

DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
status.createAndHoldSavepoint();
return status;

PROPAGATION_REQUIRED || PROPAGATION_SUPPORTS || PROPAGATION_MANDATORY

根据事务传播机制的定义,这三种传播机制在已有事务的情况下表现都是相同的:直接使用现有事务。

return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);

当前不存在事务

如果当前没有事务的话,也需要根据不同的事务传播机制分情况处理。

PROPAGATION_MANDATORY

直接抛异常。

throw new IllegalTransactionStateException(
	"No existing transaction found for transaction marked with propagation 'mandatory'");

PROPAGATION_REQUIRED || PROPAGATION_REQUIRES_NEW || PROPAGATION_NESTED

直接创建新事务执行。

DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;

PROPAGATION_SUPPORTS || PROPAGATION_NOT_SUPPORTED || PROPAGATION_NEVER

不支持事务,无需创建事务。

执行事务内操作 doInTransaction

在获取到事务对象后,就可以执行事务内包装的业务逻辑了。

这里使用了模板方法,直接执行调用方传入的 TransactionCallback 中的 doInTransaction 方法即可。

result = action.doInTransaction(status);

异常回滚 rollbackOnException

如果在执行事务内逻辑时出现了异常,需调用 transactionManager.rollback 进行回滚。

try {
    this.transactionManager.rollback(status);
}catch ( ... ) {
    ...
    throw ... ;
}

提交 commit

正常情况下,调用 transactionManager.commit 提交事务。

事务动作

开启 doBegin

代码位于 org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 该方法是真正的开启一个事务,具体流程如下:

事务绑定连接

为当前事务对象内设置使用的连接,如果当前事务内没有连接的话,就通过数据源新建一个。

注意,判断当前是否在事务中的其中一个条件就是要保存有数据库连接,见 isExistingTransaction

if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
	Connection newCon = obtainDataSource().getConnection();			
	txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

设置 read-only 标识

根据传入的事务属性设置。

如果 read-only 标识为 true,则需要将当前事务使用的数据库连接设置为 read-only

设置隔离级别

根据传入的事务属性设置。

如果当前数据库连接的隔离级别与开启事务时传入的隔离级别不一致的话,需要重新设置当前连接的隔离级别。

关闭自动提交

在事务库事务中,是需要手动调用 commit 命令进行提交的。

所以,需要将当前连接的 autoCommit 关闭。

标记事务活跃状态

txObject.getConnectionHolder().setTransactionActive(true);

注意,判断当前是否在事务中的另外一个条件就是使用的数据库连接中事务活跃状态为 true,见 isExistingTransaction

设置超时时间

根据传入的事务属性设置。

txObject.getConnectionHolder().setTimeoutInSeconds(timeout);

当前线程绑定连接

将当前连接设置进 ThreadLocal 对象中,key 为当前事务所在的数据源。

在存在嵌套事务时,或从该 ThreadLocal 中取出该连接复用, 见 doGetTransaction

TransactionSynchronizationManager.bindResource(obtainDataSource(), 
    txObject.getConnectionHolder());

挂起 suspend

* Suspend the given transaction. Suspends transaction synchronization first,
* then delegates to the {@code doSuspend} template method.

在存在嵌套事务时,挂起操作的出镜率非常高,在多种传播机制中都有使用。

而挂起,是为了完成以下需求:

  • 当前已存在事务,但子事务设置为不需要在事务中执行:PROPAGATION_NOT_SUPPORTED
  • 当前已存在事务,但子事务需要在一个全新的事务中执行:PROPAGATION_REQUIRES_NEW

所以,将当前事务挂起是为了让其不会被后续操作影响。

挂起操作如下:

清空事务内绑定连接

txObject.setConnectionHolder(null);

当前线程与连接解绑定

TransactionSynchronizationManager.unbindResource(obtainDataSource())

从步骤可以看出,事务挂起就是不执行提交,但是把资源与事务的绑定解除。

但是,挂起的资源总是要进行恢复的。所以,由挂起解除绑定的资源并不能进行释放,还需要将其传递给当前 TransactionStatus

Object suspendedResources = suspend(transaction);
...
return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources);

恢复 resume

与挂起对应,恢复挂起的事务。用于挂起当前事务后开启新事务失败,或新事务结束(rollback/commit)后的老事务恢复。

当然了,恢复 resume 就是做与挂起 suspend 相反的操作,这里就不重复了。

回滚

事务回滚入口为 org.springframework.transaction.support.AbstractPlatformTransactionManager#rollback

触发事务完成前置回调

这个是 Spring 给我们提供的一个扩展点。

判断是否有创建有检查点 / 回滚至检查点

如果当前事务内有被设置检查的的话,在执行回滚时,Spring 会帮我们回滚至检查点的位置。

Object savepoint = getSavepoint();
...
getSavepointManager().rollbackToSavepoint(savepoint);
getSavepointManager().releaseSavepoint(savepoint);
setSavepoint(null);

执行回滚 / 设置回滚

设置回滚

隔离级别是以下几个,且当前为内层事务时,会仅设置回滚,而不执行真正的回滚操作。

隔离级别不直接回滚原因
PROPAGATION_NOT_SUPPORTED内层不支持事务,无需执行回滚
PROPAGATION_NESTED与外层共用事务,使用检查点回滚
PROPAGATION_REQUIRED与外层公用事务,在内层仅设置回滚,不直接执行回滚。
PROPAGATION_SUPPORTS同上
PROPAGATION_MANDATORY同上

执行回滚

直接使用连接调用数据库进行回滚操作。

Connection con = txObject.getConnectionHolder().getConnection();
...
con.rollback();

触发事务完成后置回调

这个是 Spring 给我们提供的一个扩展点。

清理

清理 ThreadLocal 变量,恢复连接 autoCommit 、隔离级别等设置(如果需要的话),恢复挂起事务。

创建检查点

当事务隔离级别设置为 PROPAGATION_NESTED 且存在嵌套事务时,Spring 在执行嵌套事务前会为我们创建检查点。

检查点是数据库提供的功能,使我们可以在回滚时指定回滚到的位置。

检查点仅在 PROPAGATION_NESTED 隔离级别下使用。当内层事务发生回滚后,会回滚至创建出内层事务的点,而不是将整个事务回滚。

@Override
public Object createSavepoint() throws TransactionException {
	ConnectionHolder conHolder = getConnectionHolderForSavepoint();
	try {
		if (!conHolder.supportsSavepoints()) {
			throw ... ;
		}
		if (conHolder.isRollbackOnly()) {
			throw ...;
		}
		return conHolder.createSavepoint();
	}
	catch (SQLException ex) {
		throw ...;
	}
}

提交

首先,外层调用了 AbstractPlatformTransactionManager#commit 并不意味着事务提交:如果当前事务已经被设置了 rollbackOnly 的话,还是会执行回滚的。

if (defStatus.isLocalRollbackOnly()) {
	processRollback(defStatus, false);
	return;
}

提交前准备

这个是 Spring 给我们提供的一个扩展点。

触发提交前回调

这个是 Spring 给我们提供的一个扩展点。

触发事务完成前置回调

这个是 Spring 给我们提供的一个扩展点。

释放检查点

因为事务要提交了,前面保存的检查点也就没有用处了。这里会先将检查点释放(如果有的话)。

执行提交

这里会执行 commit 语句将事务提交。 但是,真正的执行提交是有条件的:

if (status.isNewTransaction()) {
    ...
    doCommit(status);
}

同回滚类似:

隔离级别不直接提交原因
PROPAGATION_NOT_SUPPORTED内层不支持事务,无需执行提交
PROPAGATION_NESTED与外层共用事务,内层不执行提交,仅释放检查点
PROPAGATION_REQUIRED与外层公用事务,不直接执行提交。
PROPAGATION_SUPPORTS同上
PROPAGATION_MANDATORY同上

触发提交后回调

这个是 Spring 给我们提供的一个扩展点。

触发事务完成后置回调

这个是 Spring 给我们提供的一个扩展点。

清理

同事务回滚的清理工作。

Spring 事务实现总结

首先,Spring 为我们提供的事务管理并不是那么神秘。

本质上是通过将数据库连接绑定至一个事务对象,并关闭该连接的 autoCommit 来实现的。

Spring 为我们提供了丰富的事务传播机制,为了实现不同的传播机制,Spring 也是用了很多花样:

PROPAGATION_REQUIRES_NEW 通过将原有事务挂起,并开启新事务执行:这样,两个事务间没有任何关系,是完全不同的两个数据库连接。所以,内层事务如果发生回滚时不会影响到外层事务的。

PROPAGATION_NESTED 还是使用原有事务,只不过创建了检查点,如果内层事务发生回滚,仅仅是回滚至检查点,外层事务不受影响。同样的,如果内层事务提交,其实并没有执行真正的提交。

image.png

其他

@startuml
start

if (当前已存在事务?) then(yes)

    split

        split
            split
                :REQUIRED;

            split again
                :SUPPORTS;

            split again
                :MANDATORY;
            end split

        end split
        :直接使用现有事务;
        note right
            newTransaction = false
        end note


    split again

        split 
            :REQUIRES_NEW;
            :挂起当前事务;
            :创建新事务;
              note right
                newTransaction = true
              end note
        split again
            :NOT_SUPPORTED;
            :挂起当前事务;
            note left
                newTransaction = false
            end note
        split again
            :NEVER;
            end
        split again
            :NESTED; 
            #00FFFF:创建检查点;
            note left
                newTransaction = false
            end note

        end split

    end split


else(no)
    split
    :MANDATORY;
        end

    split again
    :REQUIRED;
    split again
    :NESTED; 
    split again
    :REQUIRES_NEW;
    end split
    #00FA9A:创建事务;

    note right
        newTransaction = true
    end note


endif
:执行事务内操作;

if(事务成功完成?) then(yes) 
    if(检查回滚标识 rollbackOnly ? ) then(false)

    partition commit {

        if(存在检查点?) then(yes)
            #00FFFF:释放检查点;
        else if( newTransaction ? ) then(true)
            #HotPink:执行提交;
        else(false)
        endif

    }
    else(true)
        partition rollback {
            if(存在检查点?) then(yes)
                #AAAAAA:回滚至检查点;
                #00FFFF:释放检查点;
            else if( newTransaction ? ) then(true)
                #AAAAAA:执行回滚;
            else(false)
                :设置回滚标识 rollbackOnly = true;
            endif
        }
    endif
else(no)
    partition rollback {

        if(存在检查点?) then(yes)
            #AAAAAA:回滚至检查点;
            #00FFFF:释放检查点;
        else if( newTransaction ? ) then(true)
            #AAAAAA:执行回滚;
        else(false)
            :设置回滚标识 rollbackOnly = true;
        endif
    }
endif
    :清理;
    :恢复挂起事务;
stop
@enduml