事務嵌套異常rollback-only分析解決

問題

  1. 單個事務會發(fā)生rollback-only嘛阐污?嵌套事務就一定會有這個rollback-only問題嘛咱圆?
  2. 這個rollback-only是什么?為什么會拋出這么個異常隘膘?
  3. 這個rollback-only異常會有哪些實際性危害弯菊?

發(fā)生場景

之前項目線上發(fā)生報錯:“org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only”踱阿∪砩啵看到了rollback-only這幾個字佛点,大概已經(jīng)知道了是什么問題。

出錯原因

錯誤代碼舉例如下:

@Service
public class BillService {

    @Resource
    private BillMapper billMapper;
    @Resource
    private BillFeeService billFeeService;

    @Transactional(rollbackFor = Exception.class)
    public int updateStatus(String billId, BillStatusEnum billStatus) {
        billMapper.updateStatus(billId, billStatus);
        try {
            // 內部事務
            billFeeService.updateStatus(billId, billStatus);
        } catch (Exception ex) {
         // 將異常吞了 或者根據(jù)異常又做了其他事
            log.error("打印日志");
        }
        // ...其他事務
    }

}

@Service
public class BillFeeService {
    @Resource
    private BillFeeMapper billFeeMapper;

    @Transactional(rollbackFor = Exception.class)
    public int updateStatus(String billId, BillStatusEnum billFee) {
        // 模擬拋異常
    }

問題分析

上述例子使用注解方式申明事務鸳玩,而申明事務通過Spring AOP增強實現(xiàn)不跟。無論是 JDK 動態(tài)代理米碰,還是 CGLIB 動態(tài)代理吕座,都會保留了原類和增強代理類( IOC容器初始化Bean 時生成,AbstractAutoProxyCreator.postProcessAfterInitialization方法使用SingletonTargetSource來包裝bean)兩個類菱蔬。代理類仍然持有原類(CGLIB 采用繼承的方式拴泌,JDK 動態(tài)代理采用反射的方式)惊橱,把原類包裝了起來税朴,原類并沒有被增強家制。當外部調用方法時颤殴,代理類判斷這個方法是否進行事務攔截鼻忠,從而會調用到TransactionIntercptor.invoke()方法帖蔓。

AOP切入方法流程示意圖:

再看TransactionInterceptor的圖表關系:

image.png

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {

   Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

   return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

從上圖可以看出TransactionIntercptor是繼承了TransactionAspectSupport類 澈侠,實現(xiàn)了MethodInterceptor接口埋酬。事務真正處理邏輯在父類的invokeWithinTransaction方法中写妥。

而invokeWithinTransaction方法中主要做的流程:

  1. 獲取事務的屬性
  2. 加載配置中配的transactionManager
  3. 不同的事務機制使用不同的事務,如下:


    image.png
  1. 在目標方法執(zhí)行前獲取事務并收集事務信息
  2. 執(zhí)行需要增強的目標方法
  3. 出現(xiàn)異常,嘗試異常處理
  4. 提交事務前的事務信息清除
  5. 提交事務

invokeWithinTransaction流程示意圖:

流程圖2.jpeg

源碼再探

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
 final InvocationCallback invocation) throws Throwable {
  // 獲取事務屬性
    TransactionAttributeSource tas = getTransactionAttributeSource();
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);

    // 根據(jù)事務屬性確定對應的事務
    final TransactionManager tm = determineTransactionManager(txAttr);

    // 這里是判斷是反應式事務
    if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
        // ...
    }

    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    // 獲取方法唯一標識呼猪,這里的 descriptor就是在 獲取事務屬性txAttr時 設置進去的.
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    /**
    * 申明式事務
    * 我們一般申明式事務定義的是DataSourceTransactionManager
    **/
    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 創(chuàng)建 TransactionInfo 
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

        Object retVal;
        try {
            // 執(zhí)行要被增強的方法 
            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;
    }
    else {
        Object result;
        final ThrowableHolder throwableHolder = new ThrowableHolder();
        // ...
    }
}

再看createTransactionIfNecessary方法中創(chuàng)建事務信息動作過程壶唤。

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
        @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
    /**
    如果沒有指定名稱棕所,則將方法標識應用為事務名稱琳省,就是之前設置的 class.method
    **/
    if (txAttr != null && txAttr.getName() == null) {
        txAttr = new DelegatingTransactionAttribute(txAttr) {
            @Override
            public String getName() {
                return joinpointIdentification;
            }
        };
    }
    // 事務狀態(tài)
    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            // 獲取事務 
            status = tm.getTransaction(txAttr);
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
                        "] because no transaction manager has been configured");
            }
        }
    }
    /**
    * 這個方法會將新生成TransactionInfo并放入放入ThreadLocal中這樣就便于后續(xù)的還原,提交击费,回滾等一系列操作.
    * private static final ThreadLocal<TransactionInfo> transactionInfoHolder =
    * new NamedThreadLocal<>("Current aspect-driven transaction");
    */
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

AbstractPlatformTransactionManager#getTransaction:
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
      throws TransactionException {

   // Use defaults if no transaction definition given.
   // TransactionDefinition就是TransactionAttribute蔫巩,就是@Transactional注解信息
   // 表示一個事務的定義批幌,通過@Transactional注解也就是在定義一個事務
   TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

   // 獲取一個事務對象嗓节,每次都會生成一個DataSourceTransactionObject拦宣,而重點是這個對象中是否已經(jīng)存在一個數(shù)據(jù)庫連接
   Object transaction = doGetTransaction(); 
   boolean debugEnabled = logger.isDebugEnabled();

   // 判斷當前線程是否存在事務,判讀依據(jù)為當前線程記錄的連接不為空且連接中(connectionHolder)中的transactionActive屬性不為空
   if (isExistingTransaction(transaction)) {
      // 已存在事務的情況下绸罗,按不同的傳播級別進行處理
      return handleExistingTransaction(def, transaction, debugEnabled);
   }

   // 事務超時驗證 timeout如果為-1珊蟀,表示沒有時間限制
   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'");
   }
   // 這3個都會新建事務
   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 {
         boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);

         // 新建事務
         DefaultTransactionStatus status = newTransactionStatus(
               def, transaction, true, newSynchronization, debugEnabled, suspendedResources);

         // 開啟事務育灸,包括設置ConnectionHolder昵宇、隔離級別瓦哎、timout如果是新連接,綁定到當前線程
         doBegin(transaction, def);
         // 初始化TransactionSynchronizationManager中的屬性
         prepareSynchronization(status, def);
         return status;
      }
      catch (RuntimeException | Error ex) {
         resume(null, suspendedResources);
         throw ex;
      }
   }
   else {
      // 不會doBegin割岛,不會真的開啟事務蜂桶,也就是不會把Connection的autoCommit設置為false也切,sql沒有在事務中執(zhí)行
      // 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);
   }
}

completeTransactionAfterThrowing方法在拋出異常處理的動作。

  1. 依據(jù)注解上的rollbackFor = Exception.class 屬性規(guī)則進行匹配费坊,看拋出的異常是否是配置的異常實例旬痹。
  2. 第一條不滿足两残,會去匹配默認的事務屬性,看是否是RuntimeException || Error錯誤的實例進行回滾

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);
      }
      // 檢驗異常ex instanceof RuntimeException || ex instanceof Error
      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 {

         try {
           // 會嘗試提交事務,里面也有rollback-only屬性校驗
            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;
         }
      }
   }

AbsTractPlatformTransactionManager:
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 {
            // 有原有事務的話會去設置當前事務狀態(tài)
            if (status.hasTransaction()) {
                //  isGlobalRollbackOnParticipationFailure 在參與事務失敗后將現(xiàn)有事務全局標記為rollback-only
               if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                  if (status.isDebug()) {
                     logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                  }
                  // 設置當前TransactionStatus的rollback-only狀態(tài)為true
                  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);

       // 這兒拋出我們最早展示的錯誤
      if (unexpectedRollback) {
         throw new UnexpectedRollbackException(
               "Transaction rolled back because it has been marked as rollback-only");
      }
   }
   finally {
       // 最終清除事務信息
      cleanupAfterCompletion(status);
   }
}
}

最后看commitTransactionAfterReturning方法去提交事務處理的過程健芭。

// 真正commit提交處理的方法
AbstractPlatformTransactionManager:
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;
   // 這個是本地代碼手動設置rollback-only 不想以拋異常的方式回滾事務,就可以設置這個屬性
   if (defStatus.isLocalRollbackOnly()) {
      if (defStatus.isDebug()) {
         logger.debug("Transactional code has requested rollback");
      }
      processRollback(defStatus, false);
      return;
   }

   // 在最后提交的時候會校驗整個事務鏈中是否有 rollback-only標識
   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);
}

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
   try {
      boolean beforeCompletionInvoked = false;

      try {
         boolean unexpectedRollback = false;
         prepareForCommit(status);
         triggerBeforeCommit(status);
         triggerBeforeCompletion(status);
         beforeCompletionInvoked = true;
         // 事務是否存在回滾保存點
         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);
         }
          // 這里再次對rollback-only 校驗,重置unexpectedRollback狀態(tài)
         else if (isFailEarlyOnGlobalRollbackOnly()) {
            unexpectedRollback = status.isGlobalRollbackOnly();
         }

          // 這兒也會拋出UnexpectedRollbackException
         if (unexpectedRollback) {
            throw new UnexpectedRollbackException(
                  "Transaction silently rolled back because it has been marked as rollback-only");
         }
      }
      catch (UnexpectedRollbackException ex) {

}

總結

異常出現(xiàn)必要因素

  1. 出現(xiàn)嵌套事務并且所有事務的傳播機制為 PROPAGATION_REQUIRED
  2. 內嵌事務在外層被try catch后沒再往出拋

發(fā)生問題的根本原因

存在嵌套事務的場景下谴麦,內嵌事務傳播機制會影響到事務的整個提交细移,比如默認的PROPAGATION_REQUIRED機制 在已有事務的情況下會加入原先事務熊锭,形成了一個大的事務鏈碗殷。它們本應該是一體的,所有的提交應該一起成功或失敗代乃。但由于內部事務發(fā)生了異常而且還沒拋出來,這就不符合事務的特性了原茅。所以在嵌套事務中堕仔,最外層的commit會兜底去校驗多層嵌套事務鏈中是否存在rollback-only狀態(tài)來標識這個事務鏈中發(fā)生了錯誤摩骨,來保證事務的原子性。

解決應對方案

  1. 內嵌事務傳播機制可修改 PROPAGATION_REQUIRED_NEW 昌罩,需注意:此傳播方式會新建一個事務灾馒,不會加入原有的事務中你虹。
  2. 傳播機制可修改 PROPAGATION_NESTED彤避,會創(chuàng)建一個保存點,在內嵌事務發(fā)生異常后會回滾到保存點(詳細:AbstractPlatformTransactionManager#getTransaction#handleExistingTransaction
  3. 檢查多重嵌套事務代碼中的try catch董饰,很容易忽略卒暂。
  4. 盡量避免多重嵌套事務娄帖,可整合為單個大事務去提交近速。

補充

事務的傳播機制影響:


image.png
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末削葱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子昔字,更是在濱河造成了極大的恐慌首繁,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谆扎,死亡現(xiàn)場離奇詭異芹助,居然都是意外死亡状土,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來累驮,“玉大人谤专,你說我怎么就攤上這事∮持” “怎么了蜡坊?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵秕衙,是天一觀的道長。 經(jīng)常有香客問我鹦牛,道長若河,這世上最難降的妖魔是什么萧福? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮钥屈,結果婚禮上坝辫,老公的妹妹穿的比我還像新娘。我一直安慰自己竭业,他們只是感情好及舍,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布锯玛。 她就那樣靜靜地躺著攘残,像睡著了一般。 火紅的嫁衣襯著肌膚如雪遗契。 梳的紋絲不亂的頭發(fā)上实撒,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天知态,我揣著相機與錄音负敏,去河邊找鬼秘蛇。 笑死赁还,一個胖子當著我的面吹牛,可吹牛的內容都是我干的蹈胡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼却汉,長吁一口氣:“原來是場噩夢啊……” “哼合砂!你這毒婦竟也來了源织?” 一聲冷哼從身側響起谈息,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤黎茎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后踢代,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗅骄,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡溺森,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年屏积,在試婚紗的時候發(fā)現(xiàn)自己被綠了炊林。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡独榴,死狀恐怖奕枝,靈堂內的尸體忽然破棺而出隘道,到底是詐尸還是另有隱情捞烟,我是刑警寧澤题画,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布苍息,位于F島的核電站壹置,受9級特大地震影響,放射性物質發(fā)生泄漏盖喷。R本人自食惡果不足惜难咕,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一课梳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧余佃,春花似錦暮刃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至步势,卻和暖如春氧猬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背坏瘩。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留桑腮,地道東北人泉哈。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像破讨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子奕纫,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容