難道你還不知道Spring之事務的回滾和提交的原理嗎,這篇文章帶你走進原始碼級別的解讀。

碼上遇見你發表於2021-12-02

上一篇文章講解了獲取事務,並通過獲取的connection設定只讀,隔離級別等;這篇文章講事務剩下的回滾和提交。

事務的回滾處理

之前已經完成了目標方法執行前的事務準備工作。而這些準備工作的最大目的無非就是對於程式沒有按照我們期待的那樣進行,也就是出現特定的錯誤;那麼當出現錯誤的時候Spring是怎麼對資料進行恢復的呢?我們先來看一下TransactionAspectSupport類裡的invokeWithinTransaction函式的completeTransactionAfterThrowing方法(其它的已經在上一篇文章講解過,這裡不再贅述):

  • 看原始碼(TransactionAspectSupport.java)
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    // 當丟擲異常時,先判斷當前是否存在事務,這是基礎依據
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
                                    "] after exception: " + ex);
        }
        // 這裡判斷是否回滾預設的 依據是丟擲的異常是RunTimeException 或者是 Erro 型別r
        if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
            try {
                txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
            }
            catch (TransactionSystemException ex2) {
                logger.error("Application exception overridden by rollback exception", ex);
                ex2.initApplicationException(ex);
                throw ex2;
            }
            catch (RuntimeException | Error ex2) {
                logger.error("Application exception overridden by rollback exception", ex);
                throw ex2;
            }
        } else {
            // We don't roll back on this exception.
            // Will still roll back if TransactionStatus.isRollbackOnly() is true.
            // 如果不滿足回滾條件,即使丟擲異常也正常提交    
            try {
                txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
            }
            catch (TransactionSystemException ex2) {
                logger.error("Application exception overridden by commit exception", ex);
                ex2.initApplicationException(ex);
                throw ex2;
            }
            catch (RuntimeException | Error ex2) {
                logger.error("Application exception overridden by commit exception", ex);
                throw ex2;
            }
        }
    }
}
  • 原始碼分析

在對目標方法的執行過程中,一旦出現Throwable就會被引導至此方法進行處理,但是不意味著所有的Throwable都會被回滾處理;比如我麼最常用的Exception,預設是不會被處理的,預設情況下,即使出現異常,資料也會被正常提交,而這個關鍵的地方就在於txInfo.transactionAttribute.rollbackOn(ex)這個函式:

回滾條件
  • 看原始碼(DefaultTransactionAttribute.java)
@Override
public Boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}
  • 原始碼分析

從上述程式碼中可以看到,預設情況下:Spring只會對RuntimeExceptionError兩種型別的情況進行處理;但是我們可以利用註解方式來改變。例如:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
回滾處理

當然,一旦符合回滾條件,那麼Spring就會將程式引導至回滾處理函式中。接下來我們看一下回滾函式,也就是txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());程式碼的rollback方法:

  • 看原始碼(AbstractPlatformTransactionManager.java)
@Override
public final void rollback(TransactionStatus status) throws TransactionException {
    if (status.isCompleted()) {
        throw new IllegalTransactionStateException(
                            "Transaction is already completed - do not call commit or rollback more than once per transaction");
    }
    DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
    processRollback(defStatus, false);
}

看一下這個方法裡面的processRollback函式:

  • 看原始碼(AbstractPlatformTransactionManager.java)
private void processRollback(DefaultTransactionStatus status, Boolean unexpected) {
    try {
        Boolean unexpectedRollback = unexpected;
        try {
            triggerBeforeCompletion(status);
            // 如果status 有 savePoint , 說明此事務 是 PROPAGATION_NESTED 且為子事務,只能回滾到savePoint
            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Rolling back transaction to savepoint");
                }
                // 回滾到儲存點
                status.rollbackToHeldSavepoint();
            }
            // 如果此時的status顯示的是新的事務,才進行回滾 else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                    logger.debug("Initiating transaction rollback");
                }
                // 如果此時是子事務,我們在這裡想想哪些型別的事務會進到這裡
                // 回滾之前說的 已經存在的事務的處理,
                // PROPAGATION_NOT_SUPPORTED 建立的事務:
                //      return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources);
                //   說明是舊事務,並且事務為null 不會進入
                // PROPAGATION_REQUIRES_NEW 建立一個新的子事務,
                //         newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                //         說明是新事務 ,會進入這個分支
                // PROPAGATION_NESTED 建立的Status是  prepareTransactionStatus(definition, transaction, false...)是舊事物,
                //         使用的是外層的事務,不會進入
                // PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY存在事務加入事務即可,
                //         標記為舊事務,prepareTransactionStatus(definition, transaction, false..)
                // 說明當子事務,只有REQUIRES_NEW會進入到這裡進行回滾
                doRollback(status);
            } else {
                // Participating in larger transaction
                // 如果status中有事務,進入下面
                // 根據上面分析,PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY
                //         建立的Status是prepareTransactionStatus(definition, transaction, false..)
                //         如果此事務時子事務,表示存在事務,並且事務為舊事物,將進入到這裡
                if (status.hasTransaction()) {
                    if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                        }
                        // 對status中的transaction作一個回滾了的標記,並不會立即回滾
                        doSetRollbackOnly(status);
                    } else {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                        }
                    }
                } else {
                    logger.debug("Should roll back transaction but cannot - no transaction available");
                }
                // Unexpected rollback only matters here if we're asked to fail early
                if (!isFailEarlyOnGlobalRollbackOnly()) {
                    unexpectedRollback = false;
                }
            }
        }
        catch (RuntimeException | Error ex) {
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            throw ex;
        }
        triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
        // Raise UnexpectedRollbackException if we had a global rollback-only marker
        if (unexpectedRollback) {
            throw new UnexpectedRollbackException(
                                    "Transaction rolled back because it has been marked as rollback-only");
        }
    }
    finally {
        // 清空記錄的資源並將掛起的資源恢復
        // 子事務結束了,之前掛起的事務就要恢復了
        cleanupAfterCompletion(status);
    }
}

我們先看看processRollback函式裡面的status.rollbackToHeldSavepoint();這段程式碼,回滾到儲存點的程式碼,根據儲存點回滾的實現方式其實是根據底層的資料庫連線進行的。回滾到儲存點之後,也要釋放掉當前的儲存點。看具體實現:

  • 看原始碼(AbstractTransactionStatus.java)
public void rollbackToHeldSavepoint() throws TransactionException {
    Object savepoint = getSavepoint();
    if (savepoint == null) {
        throw new TransactionUsageException(
                            "Cannot roll back to savepoint - no savepoint associated with current transaction");
    }
    getSavepointManager().rollbackToSavepoint(savepoint);
    getSavepointManager().releaseSavepoint(savepoint);
    setSavepoint(null);
}
  • 原始碼分析

這裡使用的是JDBC的方式進行的資料庫連線,那麼getSavepointManager()函式返回的是JdbcTransactionObjectSupport,也就是說上面函式會呼叫JdbcTransactionObjectSupport 中的 rollbackToSavepoint 方法。

接下來檢視一下JdbcTransactionObjectSupport 中的 rollbackToSavepoint 方法。

  • 看原始碼(JdbcTransactionObjectSupport.java)
@Override
public void rollbackToSavepoint(Object savepoint) throws TransactionException {
    ConnectionHolder conHolder = getConnectionHolderForSavepoint();
    try {
        conHolder.getConnection().rollback((Savepoint) savepoint);
        conHolder.resetRollbackOnly();
    }
    catch (Throwable ex) {
        throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);
    }
}

當之前已經儲存的事務資訊中的事務為新事務,那麼直接回滾。常用於單獨事務的處理。對於沒有儲存點的回滾,Spring同樣是使用底層資料庫連線提供的API來操作的。由於我們使用的是DataSourceTransactionManager,所以AbstractPlatformTransactionManager裡的processRollback函式裡的doRollback(status);也就是在DataSourceTransactionManager實現的。

  • 看原始碼(DataSourceTransactionManager.java)
@Override
protected void doRollback(DefaultTransactionStatus status) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    Connection con = txObject.getConnectionHolder().getConnection();
    if (status.isDebug()) {
        logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
    }
    try {
        con.rollback();
    }
    catch (SQLException ex) {
        throw translateException("JDBC rollback", ex);
    }
}

當前事務資訊中表明是存在事務的,又不屬於以上兩種情況,只做回滾標識,等到提交的時候再判斷是否又回滾標識,下面回滾的時候再介紹,子事務中狀態為PROPAGATION_SUPPORTS PROPAGATION_REQUIREDPROPAGATION_MANDATORY回滾的時候將會標記為回滾標識,我們先來看看是怎麼標記的。回到AbstractPlatformTransactionManager類的processRollback函式的doSetRollbackOnly(status);

  • 看原始碼(DataSourceTransactionManager.java)
@Override
protected void doSetRollbackOnly(DefaultTransactionStatus status) {
    // 將status中的transaction取出
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    if (status.isDebug()) {
        logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() +
                            "] rollback-only");
    }
    // transaction執行標記回滾
    txObject.setRollbackOnly();
}

繼續檢視一下setRollbackOnly()函式:

  • 看原始碼(DataSourceTransactionManager.java)
public void setRollbackOnly() {
    // 這裡將transaction裡面的connHolder標記回滾
    getConnectionHolder().setRollbackOnly();
}

繼續檢視setRollbackOnly()的具體實現

  • 看原始碼(ResourceHolderSupport.java)
public void setRollbackOnly() {
    // 將holder中的這個屬性設定成true
    this.rollbackOnly = true;
}

我們看到將status中的Transaction中的ConnectionHolder的屬性rollbackOnly屬性設定為true,這裡暫時不多考慮,等到下面提交的時候再介紹。

簡單小結

簡單總結一下AbstractPlatformTransactionManager類的processRollback函式

  • status.hasSavepoint() ** 如果status中有savePoint,只回滾到savePoint!**
  • status.isNewTransaction() 如果status是一個新事務,才會真正去回滾!
  • status.hasTransaction() ** 如果status有事務,將會對staus中的事務標記!**

事務的提交

在事務的執行沒有出現任何的異常,也就意味著事務可以走正常事務的提交流程,這裡回到流程中,看看TransactionAspectSupport類中的commitTransactionAfterReturning(txInfo);函式具體做了什麼

  • 看原始碼(TransactionAspectSupport.java)
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
        }
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}
  • 原始碼分析

在真正的資料提交之前,還需要做一個判斷,不知道大家還有沒有印象,在我們分析事務異常處理規則的時候,當某個事務既沒儲存點,又不是新事務,Spring對它的處理方式只是設定一個回滾標識(具體是在AbstractPlatformTransactionManager的processRollback函式裡面)。這個標識在這裡就會派上用場了,如果子事務狀態是PROPAGATION_SUPPORTS PROPAGATION_REQUIREDPROPAGATION_MANDATORY,將會在外層事務中執行,回滾的時候,並不執行回滾,只是標記一下回滾的狀態,當外層事務提交的時候,會先判斷ConnectionHolder中的回滾狀態,如果已經標記為回滾,則不會提交,而是外層事務進行回滾。(檢視一下txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());)的commit方法:

  • 看原始碼(AbstractPlatformTransactionManager.java)
@Override
public final void commit(TransactionStatus status) throws TransactionException {
    if (status.isCompleted()) {
        throw new IllegalTransactionStateException(
                            "Transaction is already completed - do not call commit or rollback more than once per transaction");
    }
    DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
    // 如果在事務鏈中已經被標記回滾,那麼不會嘗試提交事務,直接回滾
    if (defStatus.isLocalRollbackOnly()) {
        if (defStatus.isDebug()) {
            logger.debug("Transactional code has requested rollback");
        }
        processRollback(defStatus, false);
        return;
    }
    if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
        if (defStatus.isDebug()) {
            logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
        }
        // 這裡會進行回滾,並且丟擲一個異常
        processRollback(defStatus, true);
        return;
    }
    // 如果沒有被標記回滾之類的,這裡才真正判斷是否提交
    processCommit(defStatus);
}

當事務執行一切都正常的時候,就可以真正的進入到提交流程了。

  • 看原始碼(AbstractPlatformTransactionManager.java)
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {
        Boolean beforeCompletionInvoked = false;
        try {
            Boolean unexpectedRollback = false;
            prepareForCommit(status);
            triggerBeforeCommit(status);
            triggerBeforeCompletion(status);
            beforeCompletionInvoked = true;
            // 判斷是否有savePoint
            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Releasing transaction savepoint");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                // 不提交,僅僅是釋放savePoint
                status.releaseHeldSavepoint();
            }
            // 判斷是否是新事務 else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                    logger.debug("Initiating transaction commit");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                // 這裡才真正去提交!
                doCommit(status);
            } else if (isFailEarlyOnGlobalRollbackOnly()) {
                unexpectedRollback = status.isGlobalRollbackOnly();
            }
            // Throw UnexpectedRollbackException if we have a global rollback-only
            // marker but still didn't get a corresponding exception from commit.
            if (unexpectedRollback) {
                throw new UnexpectedRollbackException(
                                            "Transaction silently rolled back because it has been marked as rollback-only");
            }
        }
        catch (UnexpectedRollbackException ex) {
            // can only be caused by doCommit
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
            throw ex;
        }
        catch (TransactionException ex) {
            // can only be caused by doCommit
            if (isRollbackOnCommitFailure()) {
                doRollbackOnCommitException(status, ex);
            } else {
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            }
            throw ex;
        }
        catch (RuntimeException | Error ex) {
            if (!beforeCompletionInvoked) {
                triggerBeforeCompletion(status);
            }
            doRollbackOnCommitException(status, ex);
            throw ex;
        }
        // Trigger afterCommit callbacks, with an exception thrown there
        // propagated to callers but the transaction still considered as committed.
        try {
            triggerAfterCommit(status);
        }
        finally {
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
        }
    }
    finally {
        // 清空記錄的資源並將掛起的資源恢復
        cleanupAfterCompletion(status);
    }
}
  • 原始碼分析
  1. status.hasSavepoint() 如果status又savePoint,說明此時的事務是巢狀事務NESTED,這個事務外面還有事務,這裡不提交,只是釋放儲存點。這裡也可以看出來NESTED的傳播行為了
  2. status.isNewTransaction() 如果是新事務才會提交!這裡如果是子事務,只有PROPAGATION_NESTED狀態才會走到這裡提交,也說明了此狀態子事務提交和外層事務是隔離的。
  3. 如果是子事務PROPAGATION_SUPPORTSPROPAGATION_REQUIREDPROPAGATION_MANDATORY這幾種狀態是舊事務,提交的時候將什麼都不做,因為它們是執行在外層事務當中,如果子事務沒有回滾,將由外層事務一次性提交

如果程式流通過了事務的層層把關,最後順利的進入了提交流程,那麼同樣,Spring會將事務提交的操作引導至底層資料庫連線的API,進行事務提交。

接下來看一下具體實現也就是AbstractPlatformTransactionManagerprocessCommit函式裡的doCommit方法

  • 看原始碼(DataSourceTransactionManager.java)
@Override
protected void doCommit(DefaultTransactionStatus status) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    Connection con = txObject.getConnectionHolder().getConnection();
    if (status.isDebug()) {
        logger.debug("Committing JDBC transaction on Connection [" + con + "]");
    }
    try {
        con.commit();
    }
    catch (SQLException ex) {
        throw translateException("JDBC commit", ex);
    }
}

從回滾和提交的邏輯看,只有status是新事務,才會進行提交或回滾,需要讀者記好這個狀態->是否是新事務

事務的清理工作

關於清理的工作我們繼續回到AbstractPlatformTransactionManagerprocessCommit函式,在這個函式裡面可以看到的是無論是在異常還是沒有異常的流程中,最後的finally程式碼塊中的都會執行這個cleanupAfterCompletion(status);方法

  • 看原始碼(AbstractPlatformTransactionManager.java)
private void cleanupAfterCompletion(DefaultTransactionStatus status) {
    // 設定完成狀態
    status.setCompleted();
    if (status.isNewSynchronization()) {
        TransactionSynchronizationManager.clear();
    }
    // 如果是新事務
    if (status.isNewTransaction()) {
        doCleanupAfterCompletion(status.getTransaction());
    }
    if (status.getSuspendedResources() != null) {
        if (status.isDebug()) {
            logger.debug("Resuming suspended transaction after completion of inner transaction");
        }
        Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
        // 結束之前事務的掛起狀態
        resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
    }
}

那麼如果是新事務呢,它會做哪些清除資源的操作呢?接下看一下cleanupAfterCompletion函式裡的doCleanupAfterCompletion函式的具體實現:

  • 看原始碼(DataSourceTransactionManager.java)
@Override
protected void doCleanupAfterCompletion(Object transaction) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    // Remove the connection holder from the thread, if exposed.
    if (txObject.isNewConnectionHolder()) {
        // 將資料庫連線從當前執行緒中解除繫結,解綁過程我們在掛起的過程中已經分析過
        TransactionSynchronizationManager.unbindResource(obtainDataSource());
    }
    // Reset connection.
    // 釋放連線,當前事務完成,則需要將連線釋放,如果有執行緒池,則重置資料庫連線,放回執行緒池
    Connection con = txObject.getConnectionHolder().getConnection();
    try {
        if (txObject.isMustRestoreAutoCommit()) {
            // 恢復資料庫連線的自動提交屬性
            con.setAutoCommit(true);
        }
        // 重置資料庫連線
        DataSourceUtils.resetConnectionAfterTransaction(
                            con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
    }
    catch (Throwable ex) {
        logger.debug("Could not reset JDBC Connection after transaction", ex);
    }
    if (txObject.isNewConnectionHolder()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Releasing JDBC Connection [" + con + "] after transaction");
        }
        // 如果當前事務是獨立的新建立的事務則在事務完成時釋放資料庫連線
        DataSourceUtils.releaseConnection(con, this.dataSource);
    }
    txObject.getConnectionHolder().clear();
}
  • 原始碼分析

綜上cleanupAfterCompletiondoCleanupAfterCompletion這兩個方法我們可以知道的是如果在事務執行前有事務掛起,那麼當前事務執行結束後需要將掛起的事務恢復。

如果有掛起的事務的話,status.getSuspendedResources()!=null為真,也就是說status中會有suspendedResource這個屬性,取得status中的transaction後計入resume方法

  • 看原始碼(AbstractPlatformTransactionManager.java)
protected final void resume(@Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder)
            throws TransactionException {
    if (resourcesHolder != null) {
        Object suspendedResources = resourcesHolder.suspendedResources;
        // 如果有被掛起的事務才會被進入
        if (suspendedResources != null) {
            // 真正去resume恢復的地方
            doResume(transaction, suspendedResources);
        }
        List<TransactionSynchronization> suspendedSynchronizations = resourcesHolder.suspendedSynchronizations;
        if (suspendedSynchronizations != null) {
            // 將上面提到的 TransactionSynchronizationManager 專門存放執行緒變數的類中的屬性設定成被掛起事務的屬性
            TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive);
            TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel);
            TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly);
            TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name);
            doResumeSynchronization(suspendedSynchronizations);
        }
    }
}

緊接著我們再去看一下真正去resume恢復的方法doResume函式

  • 看原始碼(DataSourceTransactionManager.java)
@Override
protected void doResume(@Nullable Object transaction, Object suspendedResources) {
    TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources);
}
  • 原始碼分析

我們可以看到 這裡恢復只是把suspendedResources重新繫結到執行緒中。


總結

到這裡之後,我們就把事務的回滾和提交就講完了。有興趣的童鞋可以自己再深入的瞭解一下。

喜歡的可以點贊關注支援一下,微信搜尋【碼上遇見你】及時獲取更多程式設計知識。我們共同進步。

相關文章