Spring事務捕獲異常後依舊回滾的解決

前沿

一段生產事故發人深省,在Spring的聲明式事務中手動捕獲異常,居然判定回滾瞭,這是什麼操作?話不多說直接上代碼

@Service
public class A {

    @Autowired
    private B b;

    @Autowired
    private C c;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
    public void operate() {
        try {
            b.insertB();
            c.insertC();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

@Service
public class B {

    @Autowired
    private BM bm;

    @Transactional(propagation = Propagation.REQUIRED)
    public int insertB() {
        return bm.insert("B");
    }
}

@Service
public class C {

    @Autowired
    private CM cm;

    @Transactional(propagation = Propagation.REQUIRED)
    public int insertC() {
        return cm.insert("C");
    }
}

問題闡述

好瞭大傢都看到上面這段代碼瞭,在正常的情況的我們會往B表和C表中各插入一條數據,那麼當代碼出現異常時又會怎麼樣呢?

我們現在假設B插入數據成功,但是C插入數據失敗瞭,此時異常會上拋到A,被A中operate方法的try – cache所捕獲,正常來說此時數據庫中B能插入一條記錄,而C表插入失敗,這是我們期望的情況,但事實卻不是,實際情況是B表沒有插入數據,C表也沒有插入數據,也就是說整個操作被Spring給回滾瞭

註意點
如果代碼稍稍變動一下,將try – cache放在insertC的代碼塊中,在同樣的場景下,B中會成功插入一條記錄

知識點前置條件

瞭解Spring的傳播機制的可以直接跳過

我們先要搞清楚Spring中的REQUIRED的作用
REQUIRED:如果當前沒有事務就創建一個新的事務,如果當前已經存在事務就加入到當前事務
也就是說當我們的傳播機制同時為REQUIRED時,A、B、C三者的事務是共用一個的,隻有當A的流程全部走完時才會做一次commit或者rollback操作,不會在執行B或者C的過程中進行commit和rollback

問題追蹤

好,有瞭一定的知識儲備,我們一起來看源碼
我們首先找到Spring事務的代理入口TransactionInterceptor, 當我們通過調用A類中的operate方法時會調用TransactionInterceptor的invoke方法,這是整個事務的入口,我們直接看重點invoke中的invokeWithinTransaction方法

//獲取事務屬性類 AnnotationTransactionAttributeSource
TransactionAttributeSource tas = getTransactionAttributeSource();
//獲取事務屬性
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
//獲取事務管理器
final TransactionManager tm = determineTransactionManager(txAttr);

PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
//獲取joinpoint
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
//註解事務會走這裡
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
    // Standard transaction demarcation with getTransaction and commit/rollback calls.
    //開啟事務
    TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

    Object retVal;
    try {
        // This is an around advice: Invoke the next interceptor in the chain.
        // This will normally result in a target object being invoked.
        retVal = invocation.proceedWithInvocation();
    } catch (Throwable ex) {
        // target invocation exception
        //事務回滾
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    } finally {
        cleanupTransactionInfo(txInfo);
    }

    if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
        // Set rollback-only in case of Vavr failure matching our rollback rules...
        TransactionStatus status = txInfo.getTransactionStatus();
        if (status != null && txAttr != null) {
            retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
        }
    }

    //事務提交
    commitTransactionAfterReturning(txInfo);
    return retVal;
}

不重要的代碼我已經省略瞭,好我們來看這個流程,上面這段代碼很明顯反應出瞭,當我們程序執行過程中拋出瞭異常時會調用到completeTransactionAfterThrowing的回滾操作,如果沒有發生異常最終會調用事務提交commitTransactionAfterReturning, 我們來分析一下

正常情況是C發生異常,並且執行到瞭completeTransactionAfterThrowing事務回滾,但是因為不是新創建的事務,而是加入的事務所以並不會觸發回滾操作,而在A中捕獲瞭該異常,並且最終走到commitTransactionAfterReturning事務提交,事實是這樣的嗎?

事實上就是這樣的,那就奇怪瞭,我明明提交瞭,怎麼反而回滾瞭,我們繼續往下看

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
    // Use defaults if no transaction definition given.
    TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

    //重點看.. DataSourceTransactionObject拿到對象
    Object transaction = doGetTransaction();
    boolean debugEnabled = logger.isDebugEnabled();

    //第一次進來connectionHolder為空的, 所以不存在事務
    if (isExistingTransaction(transaction)) {
        // Existing transaction found -> check propagation behavior to find out how to behave.
        //如果不是第一次進來, 則會走這個邏輯
        return handleExistingTransaction(def, transaction, debugEnabled);
    }

    // Check definition settings for new transaction.
    if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
        throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
    }

    // No existing transaction found -> check propagation behavior to find out how to proceed.
    if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
        throw new IllegalTransactionStateException(
                "No existing transaction found for transaction marked with propagation 'mandatory'");
    }
    //第一次進來大部分會走這裡(傳播屬性是 Required | Requested New | Nested)
    else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
            def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
            def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        //先掛起
        SuspendedResourcesHolder suspendedResources = suspend(null);
        if (debugEnabled) {
            logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
        }
        try {
            //開啟事務
            return startTransaction(def, transaction, debugEnabled, suspendedResources);
        } catch (RuntimeException | Error ex) {
            resume(null, suspendedResources);
            throw ex;
        }
    } else {
        // Create "empty" transaction: no actual transaction, but potentially synchronization.
        if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
            logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                    "isolation level will effectively be ignored: " + def);
        }
        boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
    }
}

這段代碼是開啟事務的代碼,我們來看,當我們A第一次走進來的時候,此時是沒有事務的,所以isExistingTransaction方法不成立,往下走,因為我們的傳播機制是REQUIRED,所以我們會走到startTransaction方法中

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    //創建一個新的事務狀態, 註意這裡的newTransaction 屬性為true
    DefaultTransactionStatus status = newTransactionStatus(
            definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    //開啟事務
    doBegin(transaction, definition);
    //開啟事務後, 改變事務狀態
    prepareSynchronization(status, definition);
    return status;
}

好這裡我們隻需要關註一個點那就是newTransactionStatus的第三個參數newTransaction,隻有當我們新創建一個事務的時候才會為true,這個屬性很重要,我們後續還會用到它

好瞭,到這裡第一次的事務開啟就已經完成瞭,然後我們會調用業務邏輯,當調用insertB時,又會走到getTransaction,我們繼續來看它,此時isExistingTransaction就可以拿到值瞭,因為A已經幫我們創建好瞭事務,此時會調用到handleExistingTransaction方法

//如果第二次進來還是PROPAFGATION_REQUIRED, 走這裡, newTransation為false
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);

針對REQUIRED有用的代碼就這一句,其他全部不用看,同樣的我們看到第三個參數newTransaction,這裡是false瞭,說明是加入瞭之前的事務,而不是自己新創建的,然後執行業務代碼,最後走到commit,我們來看看commit中做瞭什麼

//如果有回滾點
if (status.hasSavepoint()) {
    if (status.isDebug()) {
        logger.debug("Releasing transaction savepoint");
    }
    unexpectedRollback = status.isGlobalRollbackOnly();
    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();
}

它什麼事情都沒有做,為什麼?因為我們的newTransaction不為true,所以當我們的代碼在operate方法全部執行完以後才會走到這裡

好接下來我們來看insertC,前面的流程都一模一樣,我們直接看到回滾代碼

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
    try {
        boolean unexpectedRollback = unexpected;

        try {
            triggerBeforeCompletion(status);

            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Rolling back transaction to savepoint");
                }
                status.rollbackToHeldSavepoint();
            } else if (status.isNewTransaction()) {
        if (status.isDebug()) {
                    logger.debug("Initiating transaction rollback");
                }
                doRollback(status);
            } else {
                // Participating in larger transaction
                if (status.hasTransaction()) {
                    if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                        }
                        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);
    }
}

我們的insertC方法同樣它的newTransaction不是true,所以最終會走到doSetRollbackOnly,這個方法重中之重,最後會調用這樣一段代碼

public void setRollbackOnly() {
    this.rollbackOnly = true;
}

然後我們就要執行到我們的關鍵代碼A中的operate的提交代碼瞭

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);
}

好瞭,看到這大傢都明白瞭吧,在commit中,Spring會去判斷defStatus.isGlobalRollbackOnly有沒有拋出過異常被Spring所攔截,如果有,那麼就不會執行commit操作,轉而執行processRollback回滾操作

總結

在Spring的REQUIRED中,隻要異常被Spring捕獲到過,那麼Spring最終就會回滾整個事務,即使自己在業務中已經捕獲
所以我們回到最初的代碼,如果我們希望Spring不進行回滾,那麼我們隻用將try-cache方法insertC方法中就可以,因為此時拋出的異常並不會被Spring所攔截到

到此這篇關於Spring事務捕獲異常後依舊回滾的解決的文章就介紹到這瞭,更多相關Spring事務捕獲異常後回滾 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: