Spring的事物回滾問題
、概述
想必大家一想到事務,就想到ACID,或者也會想到CAP。但筆者今天不討論這個,哈哈~本文將從應用層面稍帶一點原始碼,來解釋一下我們平時使用事務遇到的一個問題但讓很多人又很棘手的問題:Transaction rolled back because it has been marked as rollback-only,中文翻譯為:事務已回滾,因為它被標記成了只回滾。囧,中文翻譯出來反倒更不好理解了,本文就針對此種事務異常做一個具體分析:
2、栗子
我們如果使用了spring來管理我們的事務,將會使事務的管理變得異常的簡單,比如如下方法就有事務:
@Transactional
@Override
public boolean create(User user) {
int i = userMapper.insert(user);
System.out.println(1 / 0); //此處丟擲異常,事務回滾,因此insert不會生效
return i == 1;
}
- 這應該是我們平時使用的一個縮影。但本文不對事務的基礎使用做討論,只討論異常情況。但本文可以給讀者導航到我的另外一篇博文,介紹了事務不生效的N種可能性:【小家java】spring事務不生效的原因大解讀
看下面這個例子,將是我們今天講述的主題:
@Transactional
@Override
public boolean create(User user) {
int i = userMapper.insert(user);
personService.addPerson(user);
return i == 1;
}
//下面是personService的addPerson方法,也是有事務的
@Transactional
@Override
public boolean addPerson(User user) {
System.out.println(1 / 0);
return false;
}
這種寫法是我們最為普通的寫法,顯然是可以回滾的。但是如果上面這麼寫:
@Transactional
@Override
public boolean create(User user) {
int i = userMapper.insert(user);
try {
personService.addPerson(user);
} catch (Exception e) {
System.out.println("不斷程式,用來輸出日誌~");
}
return i == 1;
}
這裡我們把別的service方法try住,不希望它阻斷我們的程式繼續執行。表面上看合乎情理沒毛病,but:
這裡需要注意:如果我是這麼寫:
@Transactional
@Override
public boolean addPerson(User user) {
userMapper.updateByIdSelective(user);
try {
editById(user);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Transactional
@Override
public boolean editById(User user) {
System.out.println(1 / 0);
return false;
}
也是不會產生上面所述的那個rollback-only異常的:
@Transactional
@Override
public boolean addPerson(User user) {
try {
editById(user);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Transactional
@Override
public boolean editById(User user) {
userMapper.updateByIdSelective(user);
System.out.println(1 / 0);
return false;
}
但是,我們的updateByIdSelective持久化是生效了的。分析如下:
-
為什麼update持久化生效?
因為addPerson有事務,所以editById理論上也有事務應該回滾才對,但是由於上層方法給catch住了,所以是沒有回滾的,所以持久化生效。 -
為何沒發生roolback-only的異常呢?
原因是因為editById的事務是沿用的addPerson的事務。所以其實上仍然是隻有一個事務的,所以catch住不允許回滾也是沒有任何問題的,因為事務本身是屬於addPerson的,而不屬於editById。
但是我們這麼來玩:
@Transactional
@Override
public boolean addPerson(User user) {
try {
personService.editById(user);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Transactional
@Override
public boolean editById(User user) {
userMapper.updateByIdSelective(user);
System.out.println(1 / 0);
return false;
}
就毫無疑問會丟擲如下異常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
- 1
但這麼玩,去掉addPerson方法的事務,只保留editById的事務呢?
@Override
public boolean addPerson(User user) {
try {
personService.editById(user);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Transactional
@Override
public boolean editById(User user) {
userMapper.updateByIdSelective(user);
System.out.println(1 / 0);
return false;
}
發現rollback-only異常是永遠不會出來的。
因此我們可以得出結論,rollback-only異常,是發生在異常本身才有可能出現,發生在子方法內部是不會出現的。因此這種現象最多是發生在事務巢狀裡。
備註一點:如果你catch住後繼續向上throw,也是不會出現這種情況的。
引發了這個血案。這是上面意思呢?其實很好解釋:在create準備return的時候,transaction已經被addPerson設定為rollback-only了,但是create方法給抓住消化了,沒有繼續向外丟擲,所以create結束的時候,transaction會執commit操作,所以就報錯了。看看處理回滾的原始碼:
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;
try {
triggerBeforeCompletion(status);
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
doRollback(status);
}
else {
// Participating in larger transaction
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
doSetRollbackOnly(status);
}
else {
if (status.isDebug()) {
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
}
}
}
else {
logger.debug("Should roll back transaction but cannot - no transaction available");
}
// Unexpected rollback only matters here if we're asked to fail early
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
} catch (RuntimeException | Error ex) {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
throw ex;
}
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}
}
簡單分析:addPerson()有事務,然後處理的時候有這麼一句:
這個時候把引數unexpectedRollback置為false了,所以當create事務需要回滾的時候,如下:
所以,就之前丟擲異常了,這個解釋很合理了吧。因為之前事務被設定過禁止回滾了。然後遇到了這個問題,我們有沒有解決辦法呢?其實最簡單的決絕辦法是:
@Override
public boolean addPerson(User user) {
System.out.println(1 / 0);
return false;
}
因為有原始碼裡這麼一句話:status.isNewTransaction() 所以我嘗試用一個新事務也是能解決這個問題的
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public boolean addPerson(User user) {
System.out.println(1 / 0);
return false;
}
但有時候我們並不希望是這樣子,怎麼辦呢?
這個時候其實可以不通過異常來處理,或者通過自定義異常的方式來處理。
**如果某個子方法有異常,spring將該事務標誌為rollback only。**如果這個子方法沒有將異常往上整個方法丟擲或整個方法未往上丟擲,那麼改異常就不會觸發事務進行回滾,事務就會在整個方法執行完後就會提交,這時就會造成Transaction rolled back because it has been marked as rollback-only的異常。
另外一種並不推薦的解決辦法如下:
<property name="globalRollbackOnParticipationFailure" value="false" />
- 1
這個方法也能解決,但顯然影響到全域性的事務屬性,所以極力不推薦使用。
如果isGlobalRollbackOnParticipationFailure為false,則會讓主事務決定回滾,如果當遇到exception加入事務失敗時,呼叫者能繼續在事務內決定是回滾還是繼續。然而,要注意是那樣做僅僅適用於在資料訪問失敗的情況下且只要所有操作事務能提交
Spring aop 異常捕獲原理
:被攔截的方法需顯式丟擲異常,並不能經任何處理,這樣aop代理才能捕獲到方法的異常,才能進行回滾,預設情況下aop只捕獲runtimeException的異常
換句話說:service上的事務方法不要自己try catch(或者catch後throw new runtimeExcetpion()
也成)這樣程式異常時才能被aop捕獲進而回滾。
另外一種方案:
在service層方法的catch語句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
語句,手動回滾,這樣上層就無需去處理異常(這也是比較推薦的做法)
3、使用場景
事務的場景無處不在。而這種場景一般發生在for迴圈裡面處理一些事情,但又不想被阻斷總流程,這個時候要catch的話請一定注意了
4、最後
事務被spring包裝得已經隱藏了很多細節,方便了我們的同時,也遮蔽了很多底層實現。因此有時候我們對原始碼多一些瞭解,能讓我們解決問題的時候更加的順暢
相關文章
- Spring Boot 事物回滾Spring Boot
- spring 異常 事物不回滾Spring
- Spring事務不能回滾的深層次原因Spring
- spring boot 顯示處理事務回滾Spring Boot
- 資料庫事物相關問題資料庫
- Spring Data JPA中事務回滾意外RollbackExceptionSpringException
- MySQL必知必會:簡介undo log、truncate、以及undo log如何幫你回滾事物MySql
- Spring系列之事物是如何管理的Spring
- 滾動穿透問題探索穿透
- oracle回滾溯源Oracle
- 入門Kubernetes - 滾動升級/回滾
- 深入UNDO回滾段,檢視爭用以及回滾段使用量的估算
- Kubernetes:更新與回滾
- Oracle 資料回滾Oracle
- scanf的回車干擾問題
- weex dom.scrollToElement 滾動問題
- better-scroll不能滾動問題
- CF1111D Destroy the Colony 題解 回滾揹包
- 阿里 Seata 新版本終於解決了 TCC 模式的冪等、懸掛和空回滾問題阿里模式
- 回滾與撤銷(一)
- Kubernetes:Pod 升級、回滾
- 關於 PayPal 支付回撥的問題
- 關於操作駁回遇到的問題
- sqlserver遇到回滾事務的操作策略SQLServer
- 【前端詞典】滾動穿透問題的解決方案前端穿透
- LVGL中roller滾動動畫錯亂的問題動畫
- Spring 5 中文解析資料儲存篇-理解Spring事物抽象Spring抽象
- Spring或SpringBoot中管理JFinal ORM外掛事物Spring BootORM
- Spring事物入門簡介及AOP陷阱分析Spring
- Spring框架問題分析Spring框架
- 談談 Git 程式碼回滾Git
- [20181222]如何找出回滾操作.txt
- 利用oracle的日誌挖掘實現回滾Oracle
- 拉鍊表的建立、查詢和回滾
- 淺入Kubernetes(12):Deployment 的升級、回滾
- MySQL實現事務的提交和回滾MySql
- 不能回滾的Redis事務還能用嗎Redis
- Spring基礎系列-Spring事務不生效的問題與迴圈依賴問題Spring