Spring 事務傳播屬性有那麼難嗎?看這一篇就夠了!

點擊上方“芋道源碼”,選擇“設爲星標

管她前浪,還是後浪?

能浪的浪,纔是好浪!

每天 8:55 更新文章,每天掉億點點頭髮...

源碼精品專欄

 

來源: juejin.im/post/5da6eee2f265da5bb977d65c

  • 傳播屬性

    • 環境準備

    • 異常類

    • 調用者

  • 總結

  • Github地址


學習東西要知行合一,如果只是知道理論而沒實踐過,那麼掌握的也不會特別紮實,估計過幾天就會忘記,接下來我們一起實踐來學習Spring事務的傳播屬性。

傳播屬性

傳播屬性定義的是當一個事務方法碰到另一個事務方法時的處理行爲,一共有七種行爲,定義如下

其實只看概念的話已經很直截了當了說明了每個傳播性的作用,此時我們再用具體的例子演示一下每個傳播性屬性下的行爲。

此次演示我們使用的是H2數據庫,這個數據庫是作用在內存裏面的,所以對於我們演示事務效果來說正好,無需我們在進行其他的配置了,我們新建一個表。將下面語句放在schema.sql文件裏面即可,SpringBoot程序在啓動的時候就會自動爲我們在內存裏面建立這樣的一個表。

CREATE TABLE FOO (ID INT IDENTITY, BAR VARCHAR(64));

演示之前我們會定義兩個類FooService和BarService。我們使用BarService裏面的方法進行調用FooService中的方法。

環境準備

在進行事務演示之前,其實可以分爲以下幾種情況,根據排列組合,我們可以得出以下八種情況

  • 調用者:有無事務

  • 調用者:是否有異常

  • 被調用者:有無事務**(這個是通過傳播屬性進行控制的)**所以並不在排列組合中

  • 被調用者:是否有異常

異常類

其中的RollbackException是我們自己定義的一個異常類

@Service
public class BarServiceImpl implements BarService{
    @Autowired
    private FooService fooService;

     // PROPAGATION_REQUIRED演示 無事務
    @Override
    public void testRequiredNoTransactional() throws RollbackException {
        fooService.testRequiredTransactional();
    }
}

調用者

在BarService中定義兩個方法,一個是帶着事務的,一個是不帶事務的

// 有事務
@Override
@Transactional(rollbackFor = Exception.class)
public void hasTransactional() throws RollbackException {

}

// 無事務
@Override
public void noTransactional() throws RollbackException {

}

接下來我們就根據俄上面定義的八種情況進行事務傳播屬性的學習。

PROPAGATION_REQUIRED

在此傳播屬性下,被調用方是否新建事務取決去調用者是否帶着事務。

想要了解這個傳播屬性的特性,其實我們演示上面八種情況的兩個例子就夠了

  • 第一種情況我們在被調用者拋出異常的情況下,如果查詢不到插入的數據,那麼就說明被調用者在調用者沒有事務的情況下自己新建了事務。

  • 第二種情況我們在調用者拋出異常的情況下,如果查詢不到插入的數據,那麼就說明被調用者在調用者有事務的情況下就加入當前事務了。

我們先來看一下被調用者的類的方法例子。

@Service
public class FooServiceImpl implements FooService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    // REQUIRED傳播屬性-被調用者有異常拋出
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void testRequiredHasException() throws RollbackException {
        jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ("+Global.REQUIRED_HAS_EXCEPTION+")");
        throw new RollbackException();
    }

    // REQUIRED傳播屬性-被調用者無異常拋出
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void testRequiredNoException() throws RollbackException {
        jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ("+Global.REQUIRED_NO_EXCEPTION+")");
    }
}

接下來我們看一下調用者方法的例子

@Service
public class BarServiceImpl implements BarService{
    @Autowired
    private FooService fooService;

    // 有事務
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void hasTransactional() throws RollbackException {
        // 調用者有事務,拋異常  被調用者無異常
        fooService.testRequiredNoException();
        throw new RollbackException();
    }

    // 無事務
    @Override
    public void noTransactional() throws RollbackException {
        // 調用者無事務,不拋異常  被調用者有異常
        fooService.testRequiredHasException();
    }
}

此時我們在程序調用時進行查詢

String noException = Global.REQUIRED_NO_EXCEPTION;
String hasException = Global.REQUIRED_HAS_EXCEPTION;

try {
    barService.noTransactional();
}catch (Exception e){
    log.info("第一種情況 {}",
            jdbcTemplate
                    .queryForObject("SELECT COUNT(*) FROM FOO WHERE BAR='"+hasException+"'", Long.class));
}

try {
    barService.hasTransactional();
}catch (Exception e){
    log.info("第二種情況 {}",
            jdbcTemplate
                    .queryForObject("SELECT COUNT(*) FROM FOO WHERE BAR='"+noException+"'", Long.class));
}

查看打印出來的日誌

2019-10-16 13:02:04.142  INFO 11869 --- [           main] c.e.t.t.TransactionApplication           : 第一種情況 0
2019-10-16 13:02:04.143  INFO 11869 --- [           main] c.e.t.t.TransactionApplication           : 第二種情況 0

我們看到我們都沒有查到相應的數據,說明數據都回滾了。此時我們應該就理解了那句話支持當前事務,如果沒有就新建事務。

PROPAGATION_SUPPORTS

被調用者是否有事務,完全依賴於調用者,調用者有事務則有事務,調用者沒事務則沒事務。

接下來我們還是用上面的兩個例子進行演示

  • 第一種情況:被調用者拋出異常的情況下,如果仍能查詢到數據,說明事務沒有回滾,說明被調用者沒有事務

  • 第二種情況:調用者拋出異常情況下,如果查不到數據,說明兩個方法在一個事務中

接下來仍然是例子演示

被調用者,只是將@Transactional註解中的propagation屬性更換爲了Propagation.SUPPORTS

// SUPPORTS傳播屬性-被調用者有異常拋出
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.SUPPORTS)
public void testSupportsHasException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.SUPPORTS_HAS_EXCEPTION+"')");
    throw new RollbackException();
}

// SUPPORTS傳播屬性-被調用者無異常拋出
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.SUPPORTS)
public void testSupportsNoException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.SUPPORTS_NO_EXCEPTION+"')");
}

調用者和上面的例子調用一樣,我們直接查看執行效果

2019-10-16 13:50:27.738  INFO 12174 --- [           main] c.e.t.t.TransactionApplication           : 第一種情況 1
2019-10-16 13:50:27.741  INFO 12174 --- [           main] c.e.t.t.TransactionApplication           : 第二種情況 0

我們看到了在第一種情況下查到了數據,說明在第一種情況下被調用者是沒有事務的。此時我們應該就理解了這句話 支持當前事務,如果沒有就不以事務的方式運行。

PROPAGATION_MANDATORY

依然是這兩個例子進行演示

  • 第一種情況:因爲調用者沒有事務,所以此傳播屬性下應該是拋異常的

  • 第二種情況:被調用者的事務和調用者事務是同樣的

接下來是被調用者的代碼例子

// MANDATORY傳播屬性-被調用者有異常拋出
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.MANDATORY)
public void testMandatoryHasException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.SUPPORTS_HAS_EXCEPTION+"')");
    throw new RollbackException();
}

// MANDATORY傳播屬性-被調用者無異常拋出
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.MANDATORY)
public void testMandatoryNoException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.SUPPORTS_NO_EXCEPTION+"')");
}

調用者和上面的例子調用一樣,我們直接查看執行效果

2019-10-16 13:58:39.178 ERROR 12317 --- [           main] c.e.t.t.TransactionApplication           : org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
2019-10-16 13:58:39.276  INFO 12317 --- [           main] c.e.t.t.TransactionApplication           : 第一種情況 0
2019-10-16 13:58:39.281  INFO 12317 --- [           main] c.e.t.t.TransactionApplication           : 第二種情況 0

我們發現和我們推測一樣,說明被調用者是不會自己新建事務的,此時我們應該就理解了這句話支持當前事務,如果當前沒事務就拋異常。

PROPAGATION_REQUIRES_NEW

此傳播屬性下,無論調用者是否有事務,被調用者都會新建一個事務

  • 第一種情況:調用者無事務,被調用者會新建事務,所以查不到數據

  • 第二種情況:調用者有事務,被調用者會新建一個事務,所以調用者拋異常影響不到被調用者,所以能查到數據

接下來我們演示代碼。

被調用者

// REQUIRES_NEW傳播屬性-被調用者有異常拋出
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
public void testRequiresNewHasException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.REQUIRES_NEW_HAS_EXCEPTION+"')");
    throw new RollbackException();
}

// REQUIRES_NEW傳播屬性-被調用者無異常拋出
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
public void testRequiresNewNoException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.REQUIRES_NEW_NO_EXCEPTION+"')");
}

調用者的例子和上面的相同,我們直接來看執行情況

2019-10-16 16:29:20.296  INFO 15553 --- [           main] c.e.t.t.TransactionApplication           : 第一種情況 0
2019-10-16 16:29:20.298  INFO 15553 --- [           main] c.e.t.t.TransactionApplication           : 第二種情況 1

我們發現和我們的推論是一樣的,說明調用者的事務和被調用者的事務完全無關。此時我們應該就理解這句話了無論當前是否有事務,都會新起一個事務。

PROPAGATION_NOT_SUPPORTED

無論調用者是否有事務,被調用者都不以事務的方法運行

同樣是這兩個例子

  • 第一種情況:被調用者都不會有事務,那麼在拋異常之後就能查到相應的數據

  • 第二種情況:在調用者有事務的情況下,被調用者也會在無事務環境下運行,所以我們依然能查到數據

接下來驗證我們的猜測

// NOT_SUPPORTED傳播屬性-被調用者有異常拋出
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NOT_SUPPORTED)
public void testNotSupportHasException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.NOT_SUPPORTS_HAS_EXCEPTION+"')");
    throw new RollbackException();
}

// NOT_SUPPORTED傳播屬性-被調用者無異常拋出
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NOT_SUPPORTED)
public void testNotSupportNoException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.NOT_SUPPORTS_NO_EXCEPTION+"')");
}

然後查看執行結果

2019-10-16 16:38:35.065  INFO 15739 --- [           main] c.e.t.t.TransactionApplication           : 第一種情況 1
2019-10-16 16:38:35.067  INFO 15739 --- [           main] c.e.t.t.TransactionApplication           : 第二種情況 1

我們可以看到在最後兩種情況都查到了數據,根據演示效果應該可以理解這句話了,不支持事務,如果當前存在事務,就將此事務掛起不以事務方式運行。

PROPAGATION_NEVER

調用者有事務,被調用者就會拋出異常

這個就不演示,相信大家看到這裏應該都會明白在第一種情況下我們是能夠查到數據的。在第二種情況下由於調用者帶着事務,所以會拋異常。

PROPAGATION_NESTED

此傳播屬性下,被調用者的事務是調用者的事務的子集。

我們重點說一下NESTED的傳播屬性的特性

關於什麼是嵌套事務的關係,我們用下面三個例子能夠進行演示。

  • 第一種情況:如果查不到數據,則說明在調用者無事務情況下,被調用者會新起一個事務

  • 第二種情況:如果查不到數據,說明外層事務能夠影響內層事務

  • 第三種情況:如果查到數據,說明內層事務不影響外層事務

接下來我們編寫具體的代碼

// NESTED傳播屬性-回滾事務
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void testNestedHasException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.NESTED_HAS_EXCEPTION+"')");
   // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    throw new RollbackException();
}

// NESTED傳播屬性-不回滾事務
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void testNestedNoException() throws RollbackException {
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.NESTED_NO_EXCEPTION+"')");
}

然後接下來的調用者也會有點區別

@Override
@Transactional()
public void hasTransactionalNoException() throws RollbackException {
    // NESTED傳播屬性 - 調用者有事務,不拋異常  被調用者有異常
    jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('"+Global.NESTED_HAS_EXCEPTION_TWO+"')");
    fooService.testNestedHasException();
}

然後執行效果

2019-10-16 18:01:06.387  INFO 17172 --- [           main] c.e.t.t.TransactionApplication           : 第一種情況 0
2019-10-16 18:01:06.389  INFO 17172 --- [           main] c.e.t.t.TransactionApplication           : 第二種情況 0
2019-10-16 18:01:06.390  INFO 17172 --- [           main] c.e.t.t.TransactionApplication           : 第三種情況 1

可以看出來嵌套事務的本質就是外層會影響內層,內層不影響外層。而REQUIRES_NEW則是互不影響。

總結

到現在我們已經全部分析完了七種傳播屬性,從寫這篇文章開始到結束其中也碰到過一些坑,有些是不自己實踐一遍是根本不知道的,所以我還是建議讀者看完這篇文章以後自己進行實踐,演示各種情況,只有這樣才能夠爛熟於心。

Github地址

https://github.com/modouxiansheng/Doraemon

推薦:https://github.com/YunaiV/SpringBoot-Labs



歡迎加入我的知識星球,一起探討架構,交流源碼。加入方式,長按下方二維碼噢

已在知識星球更新源碼解析如下:

最近更新《芋道 SpringBoot 2.X 入門》系列,已經 20 餘篇,覆蓋了 MyBatis、Redis、MongoDB、ES、分庫分表、讀寫分離、SpringMVC、Webflux、權限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能測試等等內容。

提供近 3W 行代碼的 SpringBoot 示例,以及超 4W 行代碼的電商微服務項目。

獲取方式:點“在看”,關注公衆號並回復 666 領取,更多內容陸續奉上。

兄弟,一口,點個????

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