一段被 try-catch 包裹後的代碼在產線穩定運行了 200 天后突然發生了異常,而這個異常居然致使了產線事務回滾。java
圖片來自 Pexels程序員
這期間究竟發生了什麼?平常在項目過程當中該如何避免事務異常?就在這個時候,老闆拿着《XX 公司關於三十歲員工優化通知》走了過來......spring
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 天沒人碰過了。
也可能已經有人猜出了問題的緣由了,這裏先賣個關子,由於這件事情裏,最重要的是這個坑是如何一步步產生的。
爲了更形象地描述這個事情我畫一個圖,紅色背景表示該方法是有事務控制的,白色背景表示該方法沒有事務:
一開始的時候,正如你們所看到的代碼,methodA 方法有事務,methodB 無事務且被 try-catch 包裹了,運行得很完美。
過了一段時間來到了階段 3,依賴 methodC 相關業務再次發生了變動,須要在 methodB 裏增長一些邏輯且須要事務控制。
通過評估確實對 methodA 沒有影響,因而通過充分測試後再次完美地上線了,然而隱藏的炸彈就在這個時候埋下了。
小夥伴們這個時候應該已經猜到緣由了,是的,你猜的沒錯。某一天 methodA 調用 methodB 時 methodB 發生了異常,因爲是繼承性事務,雖然 methodB 發生了異常被 try-catch 了,依然形成了 methodA 事務回滾。
咱們能夠把事務控制機制理解爲上圖這樣一個紅色的長長的房間,這個房間是有人看守的,他負責事務的開始、提交,還有一項重要的任務就是監控異常。
一旦發現 RuntimeException 異常直接回滾整個事務,咱們給他一個 title,稱之爲「監事」吧。
再來看階段三和一開始的代碼,方法的開頭有一個 @Transactional 註解,因而他打開了這個紅色房間的門,把 methodA 放了進去。
接着 methodB 過來了,也開啓了事務--繼承性事務,因而監事把 methodB 也安排到了這個房間。
methodB 雖然發生了異常且被 try-catch 包裹,但逃不過監事的火眼金睛,因而他按下了事務回滾的按鈕。
org.springframework.transaction.UnexpectedRollbackException: Transaction 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 方法。
@Override
public final void commit(TransactionStatus status) throws TransactionException {
// 爲便於閱讀,刪除部分代碼
......
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
// 爲便於閱讀,刪除部分代碼
proce***ollback(defStatus, true);
return;
}
processCommit(defStatus);
}
繼續往上追溯,來到 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 開啓了(聲明式)事務。
如上面這張圖所示,咱們仍是把 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 公司關於三十歲員工優化通知》放到了抽屜一疊資料的最下面,接着又抽出來放到了資料的中間。
看來個人程序生涯,又能夠持續一段時間了!