關於spring嵌套事務,我發現網上好多熱門文章持續性地以訛傳訛

事情起因是,摸魚的時候在某平臺刷到一篇spring事務相關的博文,文章最後貼了一張圖。裏面關於嵌套事務的表述明顯是錯誤的。

更奇怪的是,這張圖有點印象。在必應搜索關鍵詞PROPAGATION_NESTED出來的第一篇文章,裏面就有這這部份內容,也是結尾部份完全一模一樣。

更關鍵的是,人家原文是表格,這位倒好,估計是怕麻煩,直接給截成圖片了。

而且這篇文章其實在評論區已經被人指出來這方面的問題了,誰也不能保證自己寫的文章沒有一點紕漏,但原作者並沒有加以理會並修改錯誤。
這位轉載作者呢也不加驗證地直接拿走了。

這位轉載作者可不是個小號,而是某年度的人氣作者。
可能是有自己的公衆號,得保持一定的更新頻率?



好傢伙,沒經過驗證,一部份錯誤的內容就這樣被持續擴大傳播了。

在必應搜索關鍵詞PROPAGATION_NESTED出來文章,前兩篇都是CSDN,都是一樣的文章一樣的錯誤。另外幾篇文章也或多或少有些表述不清的地方。因此嘗試來寫一寫這方面的東西。

順便吐槽一下CSDN,我好多篇文章都被這上面的某些作者給扒過去,然後搜索一模一樣的標題,權重比我還高,出來排第一位的反而是CSDN的盜版文章。

1.當我們在談論嵌套事務的時候,嵌套的是什麼?


當看到`嵌套事務`第一反應想到是這樣式的:

但這更像PROPAGATION_REQUIRES_NEW啊,感興趣可以去打斷點執行一下。PROPAGATION_REQUIRES_NEW事務傳播下,方法A調用方法B就是這樣,

//        事務A doBegin()
//            事務B doBegin()
//            事務B doCommit()
//        事務A doCommit()
 

而在PROPAGATION_NESTED事務傳播下,打了個斷點,會發現只會執行一次doBegin和doCommit:

事務A doBegin()
事務A doCommit()

我們用代碼輸出更加直觀。
定義兩個方法serviceA和serviceB,使用前者調用後者。前者事務傳播使用REQUIRED,後者使用PROPAGATION_NESTED

@Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
            Tcity tcity2 = new Tcity();
            tcity2.setId(0);
            tcity2.setStateCode("5");
            tcity2.setCnCity("測試城市2");
            tcity2.setCountryCode("ALB");
            tcityMapper.insertSelective(tcity2);
            transactionInfo();
            test2.serviceB();
    }
 @Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
    public void serviceB() {
        Tcity tcity = new Tcity();
        tcity.setId(0);
        tcity.setStateCode("5");
        tcity.setCnCity("測試城市");
        tcity.setCountryCode("ALB");
        tcityMapper.insertSelective(tcity);
        tcityMapper.selectAll2();
        transactionInfo();

這裏的transactionInfo()使用事務同步器管理器TransactionSynchronizationManager註冊一個事務同步器TransactionSynchronization
這樣在事務完成之後afterCompletion會輸出當前事務是commit還是rollback,這樣也便於測試,比起去刷新數據庫看有沒有寫入,更加方便快捷直觀。

同時使用TransactionSynchronizationManager.getCurrentTransactionName()可以得到當前事務的名稱,這樣可以直觀的看到當前方法使用的是同一個事務還是不同的事務。

protected void transactionInfo() {

        String transactionName = TransactionSynchronizationManager.getCurrentTransactionName();
        boolean active = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("transactionName:{}, active:{}", transactionName, active);

        if (!active) {
            log.info("transaction :{} not active", transactionName);
            return;
        }
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCompletion(int status) {
                if (status == STATUS_COMMITTED) {
                    log.info("transaction :{} commit", transactionName);
                } else if (status == STATUS_ROLLED_BACK) {
                    log.info("transaction :{} rollback", transactionName);
                } else {
                    log.info("transaction :{}  unknown", transactionName);
                }
            }
        });
    }

執行測試代碼:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Test { 
    @Autowired
    private Test1 test1;

    @org.junit.Test
    public void test(){
        test1.serviceA();
    }
}

輸出:

可以非常直觀地觀察到3點情況:
1.通過上圖標記爲1的地方,可以看到兩個方法使用了一個事務com.nyp.test.service.propagation.Test1.serviceA
2.通過上圖標記爲2的地方,以及箭頭順序,可以看到事務執行順序類似於(事實上不是,只是事務同步器的問題,下文有說明):

//        事務A doBegin()
//            事務B doBegin()
//        事務A doCommit()
//            事務B doCommit()

3.通過事務同步器打印日誌發現commit執行了兩次。

以上2,3兩點與前面打斷點的結論貌似是有點衝突。


1.1嵌套事務究竟有幾個事務


源碼版本:spring-tx 5.3.25

通過源碼,可以很直觀地觀察到,useSavepointForNestedTransaction()默認返回true,這樣就不會開啓一個新的事務(startTransaction), 而是創建一個新的savepoint

相當於在方法A的時候會開啓一個新的事務,在調用方法B的時候,會在方法A之後方法B之前創建一個檢查點。

類似於在原來的A方法上手動添加檢查點。

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
        Object savePoint = null;
        try {
            Tcity tcity2 = new Tcity();
            tcity2.setId(0);
            tcity2.setStateCode("5");
            tcity2.setCnCity("測試城市2");
            tcity2.setCountryCode("ALB");
            tcityMapper.insertSelective(tcity2);
            transactionInfo();
            savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
            test2.serviceB();
        } catch (Exception exception) {
            exception.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
        }
    }


然後通過檢查點,將一個邏輯事務分爲多個物理事務
我這可不是在亂講啊,我是有備而來。


https://github.com/spring-projects/spring-framework/issues/8135
上面是spring 在github官方社區07年的一個貼子,Juergen Hoeller有一段回覆。

Juergen Hoeller是誰?他是spring的聯合創始人,事務這一塊的主要開發者。


PROPAGATION_NESTED的不同之處在於,它使用具有多個保存點的單個物理事務,可以回滾到這些保存點。這種部分回滾允許內部事務範圍觸發其範圍的回滾,而外部事務可以繼續進行物理事務,儘管已經回滾了一些操作。這通常映射到JDBC保存點上,因此只適用於JDBC資源事務(Spring的DataSourceTransactionManager)。

在嵌套事務中,整體是一個邏輯事務,通過savepoint在jdbc物理層面把調用方法分割成一個個的物理事務。
因爲spring層面只有一個邏輯事務,所以通過斷點只執行了一次doBegin()和doCommit(),但實際上執行了兩次preCommit(),如果有savepoint那就不執行commit(),
這也能回答上面2,3兩點問題的疑問。

所以上面方法A調用方法B進行嵌套事務,右(下)圖比左(上)圖更形象準確:

1.2 savepoint

savepoint是JDBC的一種機制,spring運用savepoint來實現了嵌套事務。
在數據庫操作中,默認autocommit爲true,意味着一條SQL一個事務。也可以將autocommit設置爲false,將多條SQL組成一個事務,一起commit或者rollback。
以上都是常規操作,在一個事務中所以數據庫操作全部捆綁在一起。在某些特定情況下,在一個事務中,用戶只希望rollback其中某部份,這時候可以用到savepoint。


記我們忘掉@Transactional,以編程式事務的方式來手動設置一個savepoint。

方法A,寫入一條用戶記錄,並設置一個檢查點。

    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    public void serviceA(){
        TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
        Object savePoint = null;
        try {
            Person person = new Person();
            person.setName("張三");
            personDao.insertSelective(person);
            transactionInfo();
            // 設置一個savepoint
            savePoint = status.createSavepoint();
            test2.serviceB();
        } catch (Exception exception) {
            exception.printStackTrace();
            // 這裏輸出兩次commit,到rollback到51行,會插入一條數據
            status.rollbackToSavepoint(savePoint);
            // 這裏會兩次rollback
//            platformTransactionManager.rollback(status);

        }
        platformTransactionManager.commit(status);
    }

方法B寫入一條日誌記錄。並在此模擬一個異常。

    public void serviceB() {
        TLog tLog = new TLog();
        tLog.setOprate("user");
        transactionInfo();
        tLogDao.insertSelective(tLog);     
        int a = 1 / 0;
    }

測試希望達到的效果是,日誌寫入失敗,但用戶記錄寫入成功。很明顯,如果不使用savepoint是達不到的。因爲兩個方法是一個事務,在方法B中報錯了,拋出異常,用戶和日誌的數據庫操作都將回滾。

測試輸出日誌:

[2023-04-24 14:40:18.740] INFO 88384 [main] [com.nyp.test.service.propagation.Test1] : transactionName:null, active:true
[2023-04-24 14:40:18.742] INFO 88384 [main] [com.nyp.test.service.propagation.Test2] : transactionName:null, active:true
java.lang.ArithmeticException: / by zero
	......省略
[2023-04-24 14:40:18.747] INFO 88384 [main] [com.nyp.test.service.propagation.Test1] : transaction :null commit
[2023-04-24 14:40:18.747] INFO 88384 [main] [com.nyp.test.service.propagation.Test2] : transaction :null commit

數據庫也表明用戶寫入成功,日誌寫入失敗。


2.一開始的問題,B先回滾A再正常提交?

本文開始的問題是方法A事務傳播爲PROPAGATION_REQUIRED,方法B事務傳播爲PROPAGATION_NESTED。方法A調用B,methodA正常,methodB拋異常。
這種情況下會發生什麼?

B先回滾,A再正常提交這種說法爲什麼會有問題,有什麼問題?

2.1 先B後A的順序有問題嗎?

通過前面事務同步器打印的日誌我們得知,事務以test1.serviceA()執行doBegin(),test2.serviceB()執行doBegin(),test1.serviceA()執行doCommit(),test2.serviceB()執行doCommit()這樣的順序執行。

但是果真如此嗎?

通過源碼我們首先得知,preCommit()在commit()方法之前,在preCommit()會做savepoint的判斷,如果有檢查點就不執行commit()。

  1. 同時方法B只是一個savepoint不是一個真正的事務,並不會執行事務同步器。
  2. 方法A是一個真正的事務,所以會執行commit(),同時也會執行上面的事務同步器。


這裏的事務同步器是一個Arraylist,它的執行順序即是arraylist的遍歷順序,僅僅只代表加入的先後,並不代表事務真正commit/rollback的順序。


從1,2兩點可以得出結論,先B後A的順序並沒有問題。

同時,根據1,在嵌套事務中使用事務同步器要特別小心,在檢查點的時候並不會執行同步器,同時會掩蓋真正的操作。

比如方法B回滾了,但因爲方法B只是個savepoint,所以事務同步器不會執行。等到方法A執行完操作事務同步器的時候,也只會反應外層事務即方法A的事務結果。


2.2 真正的問題

如果B回滾,A是commit還是rollback取決於方法A是否繼續把異常往上拋。


讓我們先暫時忘掉嵌套事務,測試一個REQUIRES_NEW的案例。
同樣的方法A事務傳播爲REQUIRES,方法B爲REQUIRES_NEW
此時方法A和方法B爲兩個彼此獨立的事務。
方法A調用方法B,方法B拋出異常。
此時,方法B肯定會回滾,但方法A呢?按理說彼此獨立,那肯定是commit了。


但真的如此嗎?


(1). 方法A不做異常處理。

測試結果:

可以看到確實是兩個事務,但兩個事務都rollback了。因爲方法A雖然沒有報異常,但它接到了方法B的異常且往上拋了,spring只會認爲方法A同樣也拋出了異常。因此兩個事務都需要回滾。

(2).方法A處理了異常。

將方法A代碼try-catch住,再執行。

日誌有點多不做截圖,

[2023-04-24 16:10:30.669] INFO 96664 [main] [com.nyp.test.service.propagation.Test1] : transactionName:com.nyp.test.service.propagation.Test1.serviceA, active:true
[2023-04-24 16:10:30.672] INFO 96664 [main] [com.nyp.test.service.propagation.Test2] : transactionName:com.nyp.test.service.propagation.Test2.serviceB, active:true
[2023-04-24 16:10:30.687] INFO 96664 [main] [com.nyp.test.service.propagation.Test2] : transaction :com.nyp.test.service.propagation.Test2.serviceB rollback
java.lang.ArithmeticException: / by zero
	 省略
[2023-04-24 16:10:30.689] INFO 96664 [main] [com.nyp.test.service.propagation.Test1] : transaction :com.nyp.test.service.propagation.Test1.serviceA commit

可以看到兩個單獨的事務,事務B回滾了,事務A提交了。

雖然我們這小節說的是REQUIRES_NEW,但嵌套事務是一樣的道理。

如果B回滾,當方法A繼續往上拋異常,則A回滾;當方法A處理了異常不往上拋,則A提交。

3. 場景

在2.2小節中,我們舉了REQUIRES_NEW的例子來說明,有的同學可能就會有點疑問了。既然事務B回滾了,事務A都要根據情況來判斷是否回滾,那這樣嵌套事務跟REQUIRES_NEW有啥區別?

還是拿註冊的場景來說。往數據庫寫1條用戶記錄,再寫1條註冊成功操作日誌。

  1. 如果日誌寫入失敗,用戶寫入不受影響。這種情況下, REQUIRES_NEW和嵌套事務都能實現。而且很明顯REQUIRES_NEW還沒那麼彎彎繞繞。
    2.考慮另外一種情況,如果用戶寫入失敗了,那這時候我想要日誌寫入也失敗。因爲用戶都沒了,就不存在註冊操作成功的操作日誌了。

這種場景,在方法B爲REQUIRES_NEW模式下,打印輸出

可以看到方法B提交了,也就是說用戶註冊失敗了,但用戶註冊成功的操作日誌卻寫入成功了。


我們再來看看嵌套事務的情況下:
方法A傳播級別爲REQUIRED,並模擬一個異常。

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
        Person person = new Person();
        person.setName("李四");
        personDao.insertSelective(person);
        transactionInfo();
        test2.serviceB();
        int a = 1 / 0;
    }

方法B事務傳播級別爲NESTED。

    @Transactional(propagation = Propagation.NESTED)
    public void serviceB() {
        TLog tLog = new TLog();
        tLog.setOprate("user");
        transactionInfo();
        tLogDao.insertSelective(tLog);
    }

執行日誌

可以看到同一個邏輯事務下的兩段物理事務都回滾了,達到了我們預期的效果。

4.小結

1.方法A事務傳播爲REQUIRED,方法B事務傳播爲NESTED。方法A調用方法B,當B拋出異常時,
如果A處理了異常,此時事務A提交。否則,事務A回滾。

2.REQUIRED_NEW和NESTED在有些場景下可以實現相同的功能,但在某些特定場景下只能NESTED實現。
3.NESTED底層邏輯是JDBC的savepoint。父事務類似於一個邏輯事務,savepoint將各方法分割了若干物理事務。
4.在嵌套事務中使用事務同步器時需要特別小心。


看到這裏點個讚唄`

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章