Spring的事務傳播屬性詳細解讀

  • 2019 年 10 月 30 日
  • 筆記

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

傳播屬性

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

傳播性 描述
PROPAGATION_REQUIRED 0 支援當前事務,如果沒有就新建事務
PROPAGATION_SUPPORTS 1 支援當前事務,如果沒有就不以事務的方式運行
PROPAGATION_MANDATORY 2 支援當前事務,如果當前沒事務就拋異常
PROPAGATION_REQUIRES_NEW 3 無論當前是否有事務,都會新起一個事務
PROPAGATION_NOT_SUPPORTED 4 不支援事務,如果當前存在事務,就將此事務掛起不以事務方式運行
PROPAGATION_NEVER 5 不支援事務,如果有事務就拋異常
PROPAGATION_NESTED 6 如果當前存在事務,在當前事務中再新起一個事務

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

此次演示我們使用的是H2資料庫,這個資料庫是作用在記憶體裡面的,所以對於我們演示事務效果來說正好,無需我們在進行其他的配置了,我們新建一個表。將下面語句放在schema.sql文件裡面即可,SpringBoot程式在啟動的時候就會自動為我們在記憶體裡面建立這樣的一個表。

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

演示之前我們會定義兩個類FooServiceBarService。我們使用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則是互不影響

總結

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


關注微信公眾號【程式設計師的夢想】,專註於Java,SpringBoot,SpringCloud,微服務,Docker以及前後端分離等全棧技術。

在這裡插入圖片描述