RCA: Spring 事務 rollback-only異常

一、現象

某天收到線上異常告警郵件,異常堆棧如下。

20171219-10:30:04.132 [payment_Worker-2] ERROR com.lianjia.payment.job.bill.BillJobService.execute(BillJobService.java:39) [9000020_26_592] - 支付中心-對賬單-任務 執行失敗norg.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only n at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:724) ~[spring-tx-4.3.7.RELEASE.jar!/:4.3.7.RELEASE]n at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:504) ~[spring-tx-4.3.7.RELEASE.jar!/:4.3.7.RELEASE]n at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:292) ~[spring-tx-4.3.7.RELEASE.jar!/:4.3.7.RELEASE]n at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.7.RELEASE.jar!/:4.3.7.RELEASE]n at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.7.RELEASE.jar!/:4.3.7.RELEASE]n at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.7.RELEASE.jar!/:4.3.7.RELEASE]n at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.7.RELEASE.jar!/:4.3.7.RELEASE]n at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656) ~[spring-aop-4.3.7.RELEASE.jar!/:4.3.7.RELEASE]n

線程執行某個定時任務,在事務提交時拋出了異常。看到rollback-only字樣,老司機們大概就知道問題所在了。本著嚴謹的態度,還是需要按部就班地進行分析,記錄常規的思維步驟並提煉出小的知識點。

二、分析過程:

1、如下圖AbstractPlatformTransactionManager.commit方法

  • 代碼724行的含義是:如果當前事務為新事務(最外層),或者failEarlyOnGlobalRollbackOnly開關被打開(一個標誌位,開啟了則會儘早拋出異常,默認為false),則拋出rollback-only異常。
  • 再往上看716行,由於 shouldCommitOnGlobalRollbackOnly默認實現是false,則當前defStatus.isGlobalRollbackOnly()true,即事務已被標記為全局回滾,跟蹤代碼可知全局回滾標籤是落在ResourceHolderSupport中的rollbackOnly欄位上。
  • 綜上,問題可以描述為,最外層事務在commit時發現事務已打上全局回滾標籤,於是先回滾事務——720行processRollback(defStatus),再拋出rollback-only 異常。

那麼是在哪裡打上全局回滾標籤的呢?

2、再來看業務代碼,使用的是Spring 事務註解。

在BillJobService中:

@Overridenpublic void execute(IJobContext jobContext) throws JobException {n try {n //其它邏輯n billBiz.checkBill(bill);n PayLogger.rootLogger.info("對賬單成功,billDate={}", date);n } catch (Throwable e) {n PayLogger.alarmLogger.error("支付中心-對賬單-任務 執行失敗", e);n }n}n

在BillBiz類中:

@Transactional("txManager")npublic void checkBill(Bill bill) {n try {n //其它邏輯n noticeService.checkPayStatus(payItem.getId());//支付單狀態檢驗,有db寫操作n billMapper.updateBill(bill);//更新狀態為對賬成功n } catch (Exception e) {n bill.setStatus(-1); n billMapper.updateBill(bill);//更新狀態為對賬失敗n }n}n

在NoticeServiceImpl類中:

@Transactional(value = "txManager")npublic void checkPayStatus(Long payItemId) throws Exception {n //其它邏輯n xxMapper.update(dto);n}n

  • 如代碼所示,@Transactional("txManager") 未指定事務傳播類型,默認為Propagation.REQUIRED(如果當前沒有事務,就新建一個事務,如果上下文中已經有一個事務,則加入到這個事務中,共享之) 。
  • 使用了嵌套共享事務,外層事務控制介面 billBiz.checkBill,內層事務控制介面noticeService.checkPayStatus,且內層事務會加入到外層事務,受外層控制。
  • 結合分析1,可以斷定,BillBiz代理類在執行完checkBill()方法並做事務commit時報錯了。

那麼全局回滾標籤是在外層事務打上的還是在內層事務打上的呢?

3、Spring 是使用AOP實現事務的,跟蹤到類TransactionInterceptor,再到TransactionAspectSupport,有如下片段 :

TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);//如果有必要,則創建新事務,並打開事務nObject retVal = null; ntry { n // This is an around advice: Invoke the next interceptor in the chain. n // This will normally result in a target object being invoked.n retVal = invocation.proceedWithInvocation();//執行業務邏輯 n}ncatch (Throwable ex) {//如果業務拋異常了。。。 n // target invocation exception n completeTransactionAfterThrowing(txInfo, ex); n throw ex; n}nfinally { n cleanupTransactionInfo(txInfo);n}ncommitTransactionAfterReturning(txInfo);//調用commit,提交事務nreturn retVal; n

如上,如果業務拋異常了會執行completeTransactionAfterThrowing(txInfo, ex),這個很可疑,跟蹤代碼,其內部邏輯大概如下:

  • 如果異常與註解聲明的rollBackFor 異常(默認為RuntimeException)一致,則rollback事務;
  • 其他異常,則commit事務。
  • rollback邏輯內,有多個條件分支,如果當前事務為外層事務,則真實執行事務回滾doRollback(status);如果為共享事務且為內層,則打上全局回滾標記doSetRollbackOnly(status),實際調用的是ResourceHolderSupport.setRollbackOnly()。

於是我們可以大膽的猜測,內層事務在執行介面noticeService.checkPayStatus 時拋運行時異常了,導致內層回滾並將事務打上了全局回滾標記。於是外層事務在commit時,報了rollback-only異常。

4、求證:

  • 細心的朋友會發現分析2中的業務代碼有些小問題,BillBiz.checkBill() 方法中有try catch語句,將異常吃掉了,連錯誤日誌都沒列印!!
  • 於是在catch塊中加上異常日誌,同樣的用例單測後,果然出現了運行時異常 NullPointerException ,且發生在noticeService.checkPayStatus 方法體內,而該異常是由數據過濾不嚴格引起的。
  • 真相明了了,跟上述猜測一致。

三、解決方案:

1、 既然是數據過濾不嚴格引發NullPointerException 並最終導致 rollback-only異常,那麼就從源頭修復bug,在調用noticeService.checkPayStatus 之前,增加賬單嚴格校驗的邏輯。

2、代碼BillBiz.checkBill(bill) 的業務邏輯是:給賬單bill 做對賬操作,執行正常則記對賬成功狀態,執行失敗(有異常發生)則記對賬失敗狀態;這段代碼原本的意圖是不管內層事務是commit 還是 rollback,外層事務都要commit很明顯,這是一個非共享事務的場景。於是,修改事務傳播類型為Propagation.REQUIRES_NEW(掛起當前事務,創建一個與原事務無關的新事務,儘管是由一個事務調用了另一個事務,但卻沒有父子關係,如果還沒有事務,就簡單地創建一個新事務。)

四、總結:

1、Spring聲明式事務是基於AOP實現的,用起來很簡單,使用者需要理解其運行機制,知道其所以然。

2、代碼中不能隨隨便便吃掉異常,至少也要打個log吧。


推薦閱讀:

TAG:Spring | 数据库事务 |