為什麼catch了異常,但事務還是回滾了?

程式猿DD發表於2021-07-02

前幾天我發了這篇文章《我來出個題:這個事務會不會回滾?》得到了很多不錯的反饋,也有不少讀者通過微信、群或者郵件的方式,給了我一些關於test4的回覆。其中還有直接發給我測試案例,來證明我的答案是錯的。今天,我們就來一起看看test4這個爭議很大的問題。如果您是剛開啟這篇文章,不瞭解我們在討論啥,那可以先點選檢視之前的這篇《我來出個題:這個事務會不會回滾?》。通過這兩篇文章的解析,相信你會對Spring Data JPA下的事務執行機制有質的飛躍。

為什麼沒回滾

先來說說,那些寫了程式碼驗證"不會回滾"的情況,把這些錯誤答案的原因先說清楚,然後再細說test4會回滾的情況。

根據這兩天讀者給我的案例或者描述清楚的一些情況,歸結了一下,大家寫的驗證程式碼之所以不會回滾,主要有以下三個原因:

  1. 沒有按照我題目開頭說的,採用InnoDB儲存引擎,用了MyISAM,不支援事務,自然不會復現。
  2. 沒用按照我題目開頭說的,採用JPA和JSR 303校驗註解,比如:用了MyBaits,所以自然也不會復現。
  3. 定義事務的函式不是public型別,這個基礎用法就不對了,事務本身就沒生效

歸家一下出現這些疑問的原因:沒審題事務基礎掌握不牢導致。關於事務基礎使用的一些常見注意點,之前寫過一篇文章,如果覺得這方面知識還不紮實的,建議讀一讀:《為什麼加了@Transactional註解,事務沒有回滾?》

為什麼寫了catch,還會回滾

先來看看執行時候報的異常:

javax.validation.ConstraintViolationException: Validation failed for classes [com.didispace.chapter310.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
	ConstraintViolationImpl{interpolatedMessage='個數必須在0和5之間', propertyPath=name, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Size.message}'}
]
	at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate(BeanValidationEventListener.java:140) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.onPreInsert(BeanValidationEventListener.java:80) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.action.internal.EntityInsertAction.preInsert(EntityInsertAction.java:209) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:83) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:478) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:356) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1454) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:511) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3283) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2479) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:473) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:178) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:39) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:271) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:98) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]

這個異常是這個回滾的關鍵。這個異常javax.validation.ConstraintViolationException是哪裡的呢?還記得以前說的JSR 303不?對的,是Bean Validation中的異常。

有的讀者說這個不是RuntimeException,所以不會回滾。很顯然,這類判斷的都沒有實際嘗試一下,只要點開原始碼可以馬上發現,這個異常就是屬於RunTimeException的。

實際上,之所以會回滾,與這裡使用Spring Data JPA以及Hibernate Validator有直接關係。從JPA 2.0開始,就預設支援了這些Bean Validation的實現,它提供了實體生命週期中pre-persist, pre-update,pre-remove三個事件發生時來執行校驗的功能。而在校驗的時候,當校驗失敗,丟擲javax.validation.ConstraintViolationException時,當前事務就會被標記為rollback

原始碼解析

要想了解,這其中到底發生了什麼,跟蹤原始碼是最好的方式。那麼原始碼從哪裡開始看呢?從異常日誌中找線索吧。

從異常棧中找到最近的一個錯誤,點開看看。

錯誤行數在532行tx.commit(),習慣性的加上斷點,這樣下一次進來的時候可以看看當前情況下的各種引數情況。

同時看到下面還有個catch,既然532行出錯了,那這裡肯定會進,所以也加個端點,到時候可以進去看看。

執行程式,呼叫一下test4,執行到532行,然後進入下一步,看看會到哪裡?

這個時候,會進入到org.hibernate.engine.transaction.internal.TransactionImpl,具體位置如下:

還是習慣性的,在下面兩行重要位置加上斷點,以便下次可以快速到這裡。

繼續按上看的步驟嘗試下去,可以來到下圖的位置:

可以看到校驗異常是從271行出來的,結合278行和280行,是不是清楚這裡回滾的原因了呢?

實踐出真知,當你覺得困惑的時候,不如動手寫一寫,調一調,很多答案就能自然浮現!

如果對於test4會回滾還不夠理解,或者你還有其他事務執行不如預期的讀者,那就跟著我的思路,一步步嘗試一下,可以觀察的更深入一些,你對這部分邏輯的理解就更全面了。我們正在組建高質量的Spring技術交流群,歡迎各種熱愛技術的開發者加入參與討論。這裡的每個人都有自己的閃光點,互相學習,取長補短,長期堅持,願大家都會成為自己領域裡的佼佼者!

歡迎關注我的公眾號:程式猿DD,分享外面看不到的乾貨與思考!

相關文章