1. 程式人生 > >Try-Catch包裹的程式碼異常後,竟然導致了產線事務回滾!

Try-Catch包裹的程式碼異常後,竟然導致了產線事務回滾!

>導讀:​一段被try-catch包裹後的程式碼在產線穩定運行了200天后忽然發生了異常,而這個異常竟然導致了產線事務回滾。這期間究竟發生了什麼?日常在專案過程中該如何避免事務異常?就在這個時候,老闆拿著《XX公司關於三十歲員工優化通知》走了過來...... ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020051020534681.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70)# 01 產線部分資料丟失了,因為一個蹊蹺的事務回滾。而**造成事務回滾的,竟然是一段被try-cath包裹後的程式碼,一段已經在產線穩定運行了200天的程式碼**,穩定到我們已經把它遺忘了。誰也沒想到的是,它竟然以這樣一種方式重新回到了我們的視野,宣告著它的存在! 小九九是一個永遠19歲的程式設計師,和所有程式設計師一樣地陽光、帥氣(這句話不管你信不信,反正我自己也不信。為了能夠開始今天的文章,就這麼瞎編吧,總比以“一個沒有頭髮的程式設計師”開頭的好)。當他告訴我一段try-catch的程式碼造成產線事務回滾後,我溫柔、耐心地對他說:“滾一邊去,沒看我正忙著嗎?”,然後他給我甩出了一段程式碼,用猥瑣又真誠的眼睛告訴我,他說的是真的。 # 02 我們來看一下這段導致了產線事務回滾的程式碼,類似於下面這樣的: ```java @Transactional public void main() { // 假設有多個user的操作,需要事務控制 methodA(); try { orderService.methodB(); } catch (Exception e) { // order失敗了不能影響該方法,不回滾。 // 異常處理,略 } userOtherProcess(); } ``` `methodA`方法需要事務控制,`methodB`方法不管遇到什麼異常都不能影響A事務,所以加了try-catch。可能有的人和我的第一反應一樣,是不是最後的`userOtherProcess`方法執行異常造成了`methodA`的事務回滾?小九九告訴我真的是因為`methodB`,這段程式碼當初經過嚴格的測試,而且已經200天沒人碰過了。也可能已經有人猜出了問題的原因了,這裡先賣個關子,因為這件事情裡,最重要的是這個坑是如何一步步產生的。 為了更形象地描述這個事情我畫一個圖,**紅色背景表示該方法是有事務控制的,白色背景表示該方法沒有事務**。 ![](https://img-blog.csdnimg.cn/20200510131650542.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70) 一開始的時候,正如大家所看到的程式碼,`methodA`方法有事務,`methodB`無事務且被try-catch包裹了,執行得很完美。過了一段時間後來到了階段二,因為一些需求變更新增了`methodC`,該業務也依賴了`methodB`,依然很完美地上線了。 ![](https://img-blog.csdnimg.cn/20200510132143344.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70) 過了一段時間來到了階段3,依賴`methodC`相關業務再次發生了變更,需要在`methodB`裡增加一些邏輯且需要事務控制,經過評估確實對`methodA`沒有影響,於是經過充分測試後再次完美地上線了,然而隱藏的炸彈就在這個時候埋下了。小夥伴們這個時候應該已經猜到原因了,是的,你猜的沒錯。某一天`methodA`呼叫`methodB`時`methodB`發生了異常,**由於是繼承性事務,雖然`methodB`發生了異常被try-catch了,依然造成了`methodA`事務回滾**。還沒有理解的小夥伴,可以看下面這張圖: ![](https://img-blog.csdnimg.cn/20200510201021757.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70) 我們可以把事務控制機制理解為上圖這樣一個紅色的長長的房間,這個房間是有人看守的,他負責事務的開始、提交,還有一項重要的任務就是監控異常,一旦發現RuntimeException異常直接回滾整個事務,我們給他一個title,稱之為“監事”吧。再來看階段三和一開始的程式碼,方法的開頭有一個`@Transactional`註解,於是他打開了這個紅色房間的門,把`methodA`放了進去,接著`methodB`過來了,也開啟了事務--繼承性事務,於是監事把`methodB`也安排到了這個房間,`methodB`雖然發生了異常且被try-catch包裹,但逃不過監事的火眼金睛,於是他按下了事務回滾的按鈕。 這樣理解了之後,我們再來簡單看一下原始碼: ```java org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(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行`processRollback`方法,通過Find Usages找到呼叫方`commit`方法,顯然這是一段事務提交的邏輯。 ```java @Override public final void commit(TransactionStatus status) throws TransactionException { // 為便於閱讀,刪除部分程式碼 ...... if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { // 為便於閱讀,刪除部分程式碼 processRollback(defStatus, true); return; } processCommit(defStatus); } ``` - shouldCommitOnGlobalRollbackOnly:預設實現是false,意思是如果發現事務被標記全域性回滾並且該標記不需要提交事務的話,那麼則進行回滾。 - defStatus.isGlobalRollbackOnly():判斷是否是讀取DefaultTransactionStatus中transaction物件的ConnectionHolder的rollbackOnly標誌位 繼續往上追溯,來到`TransactionAspectSupport.invokeWithinTransaction`方法: ```java @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`時事務是**不生效**的 ![](https://img-blog.csdnimg.cn/20200510143319433.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70) 如上面這張圖所示,我們還是把AOP想像成一個長方形的房間,由於`mehtodA`沒有事務,這個房間已經被標誌為沒有事務無人值守了,`mehtodB`雖然標記了事務,但很顯然是不生效的。 接下來我們重新回顧一下事務的幾種配置: - REQUIRED:支援當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。 - REQUIRES_NEW:新建事務,如果當前存在事務,把當前事務掛起。 - SUPPORTS:支援當前事務,如果當前沒有事務,就以非事務方式執行。 - MANDATORY:支援當前事務,如果當前沒有事務,就丟擲異常。 - NEVER:以非事務方式執行,如果當前存在事務,則丟擲異常。 - NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 - NESTED:支援當前事務,如果當前事務存在,則執行一個巢狀事務,如果當前沒有事務,就新建一個事務。 這方面的文章很多,這裡就不做描述了。 # 04 事務問題本身是比較難通過測試發現的,我們再來聊一聊**專案過程中如何防止事務問題的發生**。比如筆者之前曾負責過支付及資金處理相關係統,產品的單筆交易額比較大,每筆至少1萬+,正常10萬+,很多時候一筆支付就是300萬,所以容不得出現一筆資金差錯。好在我們資金交易從0做到了3000億,依然資金0差錯。針對可能的事務問題,我們採取的措施有: 1. 通過開發規範、產線坑集等文件、培訓等讓開發人員對事務有足夠的瞭解、敏感度。 2. 系統設計時,對於關鍵的業務場景需要寫明是否啟用了事務,哪些方法包裹在一個事務中,並進行評審。 3. 程式碼Review環節有很多專項Review,比如資金review、多執行緒Review等等,也有一項專門的事務Review:需不需要加事務?事務配置是否正確?異常是否處理等。 4. 開發人員構造事務異常場景進行自測、交叉驗證。 5. 測試團隊參與系統設計評審,並進行事務相關測試。比如通過防火牆阻斷請求、手動鎖表等方式來模擬可能的事務異常。 筆者在之前一家公司還有一種做法就是通過開發規範約束:所有事務的方法全部以`tx`開頭。比如`methodB`方法需要開啟事務,則新增一個`txMethodB`方法,在該方法中呼叫`methodB`。通過這種方式完全可以避免上面問題的發生,但很顯然這種方式相當地“醜陋”。 # 05 正和小九九聊著事務問題,老闆手裡拿著幾張A4紙走了過來。 作為公司唯一的30歲程式設計師,我提高了聲音對小九九說:你有沒有發現`@Transactional`中還有一個配置項`readOnly`,如果需要使用這個引數,必須啟動一個事務。但如果是讀取資料,根本就不需要事務啊?為什麼會有這麼一個自相矛盾的配置項呢?小九九一臉茫然地搖了搖頭。 老闆衝我點了點頭,轉身回到了辦公室,坐下思考了一會,然後把手裡的A4紙《XX公司​關於三十歲員工優化通知》放到了抽屜一疊資料的最下面,接著又抽出來放到了資料的中間。 看來我的程式生涯,又可以持續一段時間了! # 推薦閱讀 [Redis 6.0 新特性-多執行緒連環13問!](https://mp.weixin.qq.com/s?__biz=MzI0OTg4NDQ1Mg==&mid=2247483846&idx=1&sn=4a3b2e22a5d825c6f75de50956202cb4&chksm=e98bfb8ddefc729b70608a64240e033fa23c670bfe45b326f3445b819fb034b1769b34882915&token=745351870&lang=zh_CN#rd) [報告老闆,微服務高可用神器已祭出,您花巨資營銷的高流量來了沒?](https://mp.weixin.qq.com/s?__biz=MzI0OTg4NDQ1Mg==&mid=2247483813&idx=1&sn=2f46f3ccaf0898d4b5817e070526857c&chksm=e98bfbeedefc72f85ed1cea589fc8550ae5fa3d898c17ae1005ea090df62b2092f4370e9da2e&token=745351870&lang=zh_CN#rd) [我成功攻擊了Tomcat伺服器,大佬們的反應亮了](https://mp.weixin.qq.com/s?__biz=MzI0OTg4NDQ1Mg==&mid=2247483762&idx=1&sn=b25909118d494d60612329ad8ea56e57&chksm=e98bfb39defc722f197d8ed7b41a1e83a272db611ea1bf8ca013f25214441d91870637d89af3&token=745351870&lang=zh_CN#rd) 公眾號:**碼大叔** 資深程式設計師、架構師技術社群。 微服務 | 大資料 | 架構設計 | 技術管理。 ![](https://img2020.cnblogs.com/blog/1804963/202005/1804963-20200505210856218-1242421644.jpg)