Spring事務嵌套引發的血案---Transaction rolled back because it has been marked as rollback-only


1、概述
想必大家一想到事務,就想到ACID,或者也會想到CAP。但筆者今天不討論這個,哈哈~本文將從應用層面稍帶一點源碼,來解釋一下我們平時使用事務遇到的一個問題但讓很多人又很棘手的問題:Transaction rolled back because it has been marked as rollback-only,中文翻譯為:事務已回滾,因為它被標記成了只回滾。囧,中文翻譯出來反倒更不好理解了,本文就針對此種事務異常做一個具體分析:

看此篇博文之前,建議先閱讀:【小家java】Spring事務不生效的原因大解讀

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持久化是生效了的。分析如下:

1,為什么update持久化生效?
因為addPerson有事務,所以editById理論上也有事務應該回滾才對,但是由於上層方法給catch住了,所以是沒有回滾的,所以持久化生效。

2,為何沒發生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" />

這個方法也能解決,但顯然影響到全局的事務屬性,所以極力不推薦使用。

如果isGlobalRollbackOnParticipationFailure為false,則會讓主事務決定回滾,如果當遇到exception加入事務失敗時,調用者能繼續在事務內決定是回滾還是繼續。然而,要注意是那樣做僅僅適用於在數據訪問失敗的情況下且只要所有操作事務能提交

Tips:
Spring aop 異常捕獲原理:被攔截的方法需顯式拋出異常,並不能經任何處理,這樣aop代理才能捕獲到方法的異常,才能進行回滾,默認情況下aop只捕獲runtimeException的異常

換句話說:service上的事務方法不要自己try catch(或者catch后throw new runtimeExcetpion()也成)這樣程序異常時才能被aop捕獲進而回滾。

另外一種方案:
在service層方法的catch語句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();語句,手動回滾,這樣上層就無需去處理異常(這也是比較推薦的做法)

transactionManagerService.rollback(transactionManager, transactionStatus);
//假如外層有調用的話,就設置,假如沒有外層調用,就不用設置
if (headDTO.isNeedRollback()) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

 

3、使用場景
事務的場景無處不在。而這種場景一般發生在for循環里面處理一些事情,但又不想被阻斷總流程,這個時候要catch的話請一定注意了。


原文鏈接:https://blog.csdn.net/f641385712/article/details/80445912


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM