一段被Try-Catch包裹的代碼,差點讓我丟了工做!


一段被 try-catch 包裹後的代碼在產線穩定運行了 200 天后突然發生了異常,而這個異常居然致使了產線事務回滾。java


image.png

圖片來自 Pexels程序員


這期間究竟發生了什麼?平常在項目過程當中該如何避免事務異常?就在這個時候,老闆拿着《XX 公司關於三十歲員工優化通知》走了過來......spring

image.png

01多線程


產線部分數據丟失了,由於一個蹊蹺的事務回滾。而形成事務回滾的,居然是一段被 try-cath 包裹後的代碼,一段已經在產線穩定運行了 200 天的代碼,穩定到咱們已經把它遺忘了。app


誰也沒想到的是,它居然以這樣一種方式從新回到了咱們的視野,宣告着它的存在!ide


小九九是一個永遠 19 歲的程序員,和全部程序員同樣地陽光、帥氣(這句話無論你信不信,反正我本身也不信。爲了可以開始今天的文章,就這麼瞎編吧,總比以「一個沒有頭髮的程序員」開頭的好)。測試


當他告訴我一段 try-catch 的代碼形成產線事務回滾後,我溫柔、耐心地對他說:「滾一邊去,沒看我正忙着嗎?」,而後他給我甩出了一段代碼,用猥瑣又真誠的眼睛告訴我,他說的是真的。優化


02spa


咱們來看一下這段致使了產線事務回滾的代碼,相似於下面這樣的:
@Transactional
public void main() {
    // 假設有多個user的操做,須要事務控制
    methodA();

    try {
        orderService.methodB();
    } catch (Exception e) {
        // order失敗了不能影響該方法,不回滾。
        // 異常處理,略
    }
    userOtherProcess();
}

methodA 方法須要事務控制,methodB 方法無論遇到什麼異常都不能影響 A 事務,因此加了 try-catch。線程


可能有的人和個人第一反應同樣,是否是最後的 userOtherProcess 方法執行異常形成了 methodA 的事務回滾?


小九九告訴我真的是由於 methodB,這段代碼當初通過嚴格的測試,並且已經 200 天沒人碰過了。


也可能已經有人猜出了問題的緣由了,這裏先賣個關子,由於這件事情裏,最重要的是這個坑是如何一步步產生的。


爲了更形象地描述這個事情我畫一個圖,紅色背景表示該方法是有事務控制的,白色背景表示該方法沒有事務:

image.png

一開始的時候,正如你們所看到的代碼,methodA 方法有事務,methodB 無事務且被 try-catch 包裹了,運行得很完美。


過了一段時間後來到了階段二,由於一些需求變動新增了 methodC,該業務也依賴了 methodB,依然很完美地上線了。 image.png

過了一段時間來到了階段 3,依賴 methodC 相關業務再次發生了變動,須要在 methodB 裏增長一些邏輯且須要事務控制。


通過評估確實對 methodA 沒有影響,因而通過充分測試後再次完美地上線了,然而隱藏的炸彈就在這個時候埋下了。


小夥伴們這個時候應該已經猜到緣由了,是的,你猜的沒錯。某一天 methodA 調用 methodB 時 methodB 發生了異常,因爲是繼承性事務,雖然 methodB 發生了異常被 try-catch 了,依然形成了 methodA 事務回滾。


尚未理解的小夥伴,能夠看下面這張圖:

image.png

咱們能夠把事務控制機制理解爲上圖這樣一個紅色的長長的房間,這個房間是有人看守的,他負責事務的開始、提交,還有一項重要的任務就是監控異常。


一旦發現 RuntimeException 異常直接回滾整個事務,咱們給他一個 title,稱之爲「監事」吧。


再來看階段三和一開始的代碼,方法的開頭有一個 @Transactional 註解,因而他打開了這個紅色房間的門,把 methodA 放了進去。


接着 methodB 過來了,也開啓了事務--繼承性事務,因而監事把 methodB 也安排到了這個房間。


methodB 雖然發生了異常且被 try-catch 包裹,但逃不過監事的火眼金睛,因而他按下了事務回滾的按鈕。


這樣理解了以後,咱們再來簡單看一下源碼:
org.springframework.transaction.UnexpectedRollbackExceptionTransaction rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.proce***ollback(AbstractPlatformTransactionManager.java:873)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:710)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:534)

根據異常提示,能夠看到錯誤發生在 AbstractPlatformTransactionManager 的 873 行 proce***ollback 方法。


經過 Find Usages 找到調用方 commit 方法,顯然這是一段事務提交的邏輯。
@Override
public final void commit(TransactionStatus status) throws TransactionException {
    // 爲便於閱讀,刪除部分代碼
    ......
 if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
  // 爲便於閱讀,刪除部分代碼
  proce***ollback(defStatus, true);
  return;
 }
 processCommit(defStatus);
}


shouldCommitOnGlobalRollbackOnly: 默認實現是 false,意思是若是發現事務被標記全局回滾而且該標記不須要提交事務的話,那麼則進行回滾。
defStatus.isGlobalRollbackOnly(): 判斷是不是讀取 DefaultTransactionStatus 中 transaction 對象的 ConnectionHolder 的 rollbackOnly 標誌位。

繼續往上追溯,來到 TransactionAspectSupport.invokeWithinTransaction 方法:

@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
  final InvocationCallback invocation)
 throws Throwable 
{
 // 爲便於閱讀,刪除部分代碼
    ......
    // 若是是聲明式事務
 if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
  // Standard transaction demarcation with getTransaction and commit/rollback calls.
  TransactionInfo txInfo = createTransactionIfNecessary(tm, 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) {
   // 捕獲異常,並將會把事務設置爲Rollback回滾狀態。
   completeTransactionAfterThrowing(txInfo, ex);
   throw ex;
  }
  finally {
   cleanupTransactionInfo(txInfo);
  }
  // 提交事務
  commitTransactionAfterReturning(txInfo);
  return retVal;
 }

 else {
  // 聲明式事務,略
 }
}

整個執行過程參見注釋說明,其它源碼就不羅列了。Spring 捕獲異常後,正如咱們所猜想的,事務將會被設置全局 rollback。


而最外層的事務方法執行 commit 操做,這時因爲事務狀態爲 rollback,Spring 認爲不該該 commit 提交事務,而應該回滾事務,因此拋出 rollback-only 異常。


03


還有一個比較典型的事務問題就是:在同一個類中,mehtodA 沒有事務,mehtodB 開啓了(聲明式)事務。


此時 mehtodA 調用 mehtodB 時事務是不生效的:

image.png

如上面這張圖所示,咱們仍是把 AOP 想像成一個長方形的房間,因爲 mehtodA 沒有事務,這個房間已經被標誌爲沒有事務無人值守了,mehtodB 雖然標記了事務,但很顯然是不生效的。


接下來咱們從新回顧一下事務的幾種配置:

  • REQUIRED:支持當前事務,若是當前沒有事務,就新建一個事務。這是最多見的選擇。

  • REQUIRES_NEW:新建事務,若是當前存在事務,把當前事務掛起。

  • SUPPORTS:支持當前事務,若是當前沒有事務,就以非事務方式執行。

  • MANDATORY:支持當前事務,若是當前沒有事務,就拋出異常。

  • NEVER:以非事務方式執行,若是當前存在事務,則拋出異常。

  • NOT_SUPPORTED:以非事務方式執行操做,若是當前存在事務,就把當前事務掛起。

  • NESTED:支持當前事務,若是當前事務存在,則執行一個嵌套事務,若是當前沒有事務,就新建一個事務。


這方面的文章不少,這裏就不作描述了。


04


事務問題自己是比較難經過測試發現的,咱們再來聊一聊項目過程當中如何防止事務問題的發生。


好比筆者以前曾負責過支付及資金處理相關係統,產品的單筆交易額比較大,每筆至少 1 萬+,正常 10 萬+,不少時候一筆支付就是 300 萬,因此容不得出現一筆資金差錯。好在咱們資金交易從 0 作到了 3000 億,依然資金 0 差錯。


針對可能的事務問題,咱們採起的措施有:

  • 經過開發規範、產線坑集等文檔、培訓等讓開發人員對事務有足夠的瞭解、敏感度。

  • 系統設計時,對於關鍵的業務場景須要寫明是否啓用了事務,哪些方法包裹在一個事務中,並進行評審。

  • 代碼 Review 環節有不少專項 Review,好比資金 Review、多線程 Review 等等,也有一項專門的事務 Review:需不須要加事務?事務配置是否正確?異常是否處理等。

  • 開發人員構造事務異常場景進行自測、交叉驗證。

  • 測試團隊參與系統設計評審,並進行事務相關測試。好比經過防火牆阻斷請求、手動鎖表等方式來模擬可能的事務異常。


筆者在以前一家公司還有一種作法就是經過開發規範約束:全部事務的方法所有以 tx 開頭。


好比 methodB 方法須要開啓事務,則新增一個 txMethodB 方法,在該方法中調用 methodB。經過這種方式徹底能夠避免上面問題的發生,但很顯然這種方式至關地「醜陋」。


05


正和小九九聊着事務問題,老闆手裏拿着幾張 A4 紙走了過來。


做爲公司惟一的 30 歲程序員,我提升了聲音對小九九說:你有沒有發現 @Transactional 中還有一個配置項 readOnly,若是須要使用這個參數,必須啓動一個事務。


但若是是讀取數據,根本就不須要事務啊?爲何會有這麼一個自相矛盾的配置項呢?小九九一臉茫然地搖了搖頭。


老闆衝我點了點頭,轉身回到了辦公室,坐下思考了一會,而後把手裏的 A4 紙《XX 公司關於三十歲員工優化通知》放到了抽屜一疊資料的最下面,接着又抽出來放到了資料的中間。


看來個人程序生涯,又能夠持續一段時間了!

相關文章
相關標籤/搜索