Spring的事物回滾問題

Start afresh發表於2020-12-15

、概述

想必大家一想到事務,就想到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持久化是生效了的。分析如下:

  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" />
  • 1

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

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

Tips:

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

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

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

3、使用場景

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

4、最後

事務被spring包裝得已經隱藏了很多細節,方便了我們的同時,也遮蔽了很多底層實現。因此有時候我們對原始碼多一些瞭解,能讓我們解決問題的時候更加的順暢

相關文章