Spring Transaction

詳細瞭解 Spring Transaction 的 Propagation

羅國榮 2021/12/30 09:25:39
21907

前言

在處理Mantis維修單時,遇到了一個與Transaction(交易)有關的錯誤。發生錯誤的原因是:要即刻Update(更新)資料至資料庫表格的行為,被"延後"至Transaction(交易)結束時才真正執行。導致系統執行的結果與預期完全不同。

上述功能的情境是「允許多人同時搶買商品,而商品有數量上限,商品賣完就停止交易」。此情境下,準確扣減商品數量最為重要。

當商品數量記錄在資料庫表格,「扣減商品數量」需要先從資料庫表格讀出商品數量;扣減後再將商品數量更新至資料庫表格。此時若是使用了Spring @Transaction(交易)機制,又對Spring @Transaction的Propagation(傳播)特性不瞭解,就會造成不預期的錯誤,導致商品數量沒及時更新或是出現Exception(例外)導致失敗。

基於上述類似的問題很容易出現在各種商業領域的情境中,所以全面地詳細測試Spring @Transaction的各種Propagation(傳播)特性是有必要的。藉著此次機會將測試結果記錄下來,做為日後查閱之用。

提高Transaction傳播特性的瞭解後,期許能夠在編寫交易類程式時更得心應手,更穩健踏實地開發出穩定的系統。

 

Propagation Types 傳播類型

甚麼是Propagation(傳播)呢?當一個Transaction方法(method)碰到另一個Transaction方法(method)時的處理行為。

Propagation Types(傳播類型)一共有八種,說明如下:

序號

Propagation Types & Description
傳播類型與說明

1

TransactionDefinition.PROPAGATION_MANDATORY

◆說明--
Supports a current transaction; throws an exception if no current transaction exists.
(
支持當前 Transaction;如果當前 Transaction 不存在,則引發異常。)

Spring
org.springframework.transaction.annotation.Propagation.MANDATORY

2

TransactionDefinition.PROPAGATION_NESTED

◆說明--
Executes within a nested transaction if a current transaction exists.
(
如果當前 Transaction 存在,則在嵌套 Transaction 中執行。)

Spring
org.springframework.transaction.annotation.Propagation.
NESTED

3

TransactionDefinition.PROPAGATION_NEVER

◆說明--
Does not support a current transaction; throws an exception if a current transaction exists.
(
不支持當前 Transaction;如果當前 Transaction 存在,則引發異常。)

Spring
org.springframework.transaction.annotation.Propagation.NEVER

4

TransactionDefinition.PROPAGATION_NOT_SUPPORTED

◆說明--
Does not support a current transaction; rather always execute nontransactionally.
(
不支持當前 Transaction;而是始終以非 Transaction 方式執行。)

Spring
org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED

5

(常用)

TransactionDefinition.PROPAGATION_REQUIRED

◆說明--
Supports a current transaction; creates a new one if none exists.
(
支持當前 Transaction;如果不存在,則創建一個新的。)

Spring
org.springframework.transaction.annotation.Propagation.REQUIRED

This is the default setting of a transaction annotation.
(Spring @Transaction Propagation
屬性之預設值)

6

(常用)

TransactionDefinition.PROPAGATION_REQUIRES_NEW

◆說明--
Creates a new transaction, suspending the current transaction if one exists.
(
創建一個新 Transaction,如果 Transaction 存在則暫停當前 Transaction )

Spring
org.springframework.transaction.annotation.Propagation.REQUIRES_NEW

7

TransactionDefinition.PROPAGATION_SUPPORTS

◆說明--
Supports a current transaction; executes non-transactionally if none exists.
(
支持當前 Transaction;如果不存在,則以非 Transaction 方式執行。)

Spring
org.springframework.transaction.annotation.Propagation.SUPPORTS

8

TransactionDefinition.TIMEOUT_DEFAULT

◆說明--
Uses the default timeout of the underlying transaction system, or none if timeouts are not supported.
(
使用基礎 Transaction 系統的默認超時;如果不支持超時,則不使用默認超時。)

Spring 不支援

第五個傳播類型 REQUIRED 和第六個傳播類型 REQUIRES_NEW 這二種傳播類型是最常用的,可以多留意以下範例的測試結果,可有效避免再次踏入陷阱。

 

傳播的情境

當一個交易方法(method)碰到另一個交易方法(method)時,調用者與被調用者是否有事務(Transaction)?是否發生異常(Exception)?都會影響到 Transaction 是否回滾(Rollback)?以下列出互相影響的八種情境。

序號

調用者有交易?

調用者有異常?

被調用者有交易?

被調用者有異常?

1

被調用者是否有 Transaction,取決於@Transaction  Propagation 屬性的屬性值

2

沒有

3

沒有

4

沒有

沒有

5

沒有

6

沒有

沒有

7

沒有

沒有

8

沒有

沒有

沒有

表格中標示藍色字體的情境共有三種,這三種情境最具代表性,也會在下述傳播類型與情境案例說明中,搭配Transaction傳播類型逐一說明。

 

傳播類型與情境案例說明

  • 以下案例說明,調用者函式與被調用者函式皆是使用註解式Transaction。
  • 在傳播類型案例的情境上,被調用者函式一定會使用@Transactional(propagation=Propagation.xxx),而 xxx 意指傳播類型。

 

1. PROPAGATION_MANDATORY (支持當前 Transaction;如果當前 Transaction 不存在,則引發異常)

※在此傳播屬性下,調用者有 Transaction,則被調用者使用該 Transaction,否則引發異常。

當被調用者函式使用 PROPAGATION_MANDATORY 此傳播類型時,用以下的情境進行說明 Transaction 是否會回滾(Rollback):

情境一

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,開啟事務

  1. 調用者事務,被調用者開啟事務。
  2. 被調用者拋出異常被調用者回滾
    1. 調用者沒有捕獲異常,則調用者回滾
    2. 調用者捕獲異常並正常提交事務,則會發生 Transaction silently rolled back because it has been marked as rollback-only 的異常。

Case2-a:

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.MANDATORY)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

Case2-b:

<調用者>

@Transactional()
public void hasTransactional() {
    try{
        insertData();
        callee.hasTransactional();
    } catch(Exception e) {
    }
}

<被調用者(callee)>

@Transactional(propagation = Propagation.MANDATORY)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

情境二

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,開啟事務

  1. 調用者事務,被調用者開啟事務。
  2. 調用者拋出異常,因被調用者的事務與調用者是同一個,所以調用者被調用者皆會回滾

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
    throw new RuntimeException();
}

<被調用者(callee)>

@Transactional(propagation = Propagation.MANDATORY)
public void hasTransactional() {
    insertData();
}

情境三

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

  1. 調用者事務,因被調用者的傳播類型為 MANDATORY;調用者呼叫被調用者函式時,發生Exception:No existing transaction found for transaction marked with propagation 'mandatory'。

<調用者>

public void noTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.MANDATORY)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

2. PROPAGATION_NESTED (如果當前 Transaction 存在,則在嵌套事務中執行)

※在此傳播屬性下,被調用者 Transaction 與調用者 Transaction 有嵌套關係。嵌套事務的本質就是外層會影響內層,內層不影響外層。

我們重點說一下 NESTED 傳播類型的特性:

調用者是否有事務

說明

被調用者會新起一個 Transaction ,此 Transaction 和調用者 Transaction 是一個嵌套的關係

被調用者會自己新起一個 Transaction 

 

當被調用者函式使用 PROPAGATION_NESTED 此傳播類型時,用以下的情境進行說明 Transaction 是否會回滾(Rollback):

情境一

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,新建事務

  1. 調用者事務,被調用者新建事務。
  2. 被調用者拋出異常被調用者回滾
    1. 調用者沒有捕獲異常,則調用者被調用者回滾
    2. 調用者捕獲異常,則調用者不回滾被調用者回滾
  • 觀察程式執行結果,被調用者拋出異常,調用者不回滾;被調用者回滾。
  • 調用者有事務,被調用者新建事務,二者事務成為嵌套關係,調用者事務不受被調用者事務影響。
  • 結論:Propagation.NESTED傳播類型,外層事務不受內層事務影響

Case2-a:

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.NESTED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

Case2-b:

<調用者>

@Transactional()
public void hasTransactional() {
    try{
        insertData();
        callee.hasTransactional();
    } catch(Exception e) {
    }
}

<被調用者(callee)>

@Transactional(propagation = Propagation.NESTED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

情境二

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,新建事務

  1. 調用者事務,被調用者新建事務。
  2. 調用者拋出異常,調用者被調用者皆會回滾

  • 調用者有事務,被調用者新建事務,二者事務成為嵌套關係。
  • 調用者拋出異常,因調用者與被調用者的事務不是同一個,理論上被調用者事務不會回滾才對,但是最終結果被調用者事務回滾了。
  • 結論:Propagation.NESTED傳播類型,外層事務會影響內層事務

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
    throw new RuntimeException();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.NESTED)
public void hasTransactional() {
    insertData();
}

情境三

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,新建事務

  1. 調用者事務,被調用者新建事務。
  2. 被調用者拋出異常,調用者不回滾被調用者回滾

<調用者>

public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

<被調用者(callee)>

@Transactional(propagation = Propagation.NESTED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

3. PROPAGATION_NEVER(不支持當前 Transaction;如果當前 Transaction 存在,則引發異常)

※在此傳播屬性下,調用者有 Transaction,被調用者就會拋出異常。

 

當被調用者函式使用 PROPAGATION_NEVER 此傳播類型時,用以下的情境進行說明 Transaction 是否會回滾(Rollback):

情境一/情境二

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

無/有

無,Never

無/引發異常
  1. 調用者事務,被調用者則會出現 Existingtransaction found for transaction marked with propagation 'never' 異常

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.NEVER)
public void hasTransactional() {
    insertData();
}

情境三

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

  1. 調用者事務,被調用者事務。
  2. 被調用者拋出異常,因調用者與被調用者皆無事務,所以調用者被調用者皆不會回滾。

 

<調用者>

public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.NEVER)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

4. PROPAGATION_NOT_SUPPORTED (不支持當前 Transaction;而是始終以非 Transaction 方式執行)

※在此傳播屬性下,無論調用者是否有 Transaction,被調用者都不以 Transaction 的方式運行。

 

當被調用者函式使用 PROPAGATION_NOT_SUPPORTED 此傳播類型時,用以下的情境進行說明 Transaction 是否會回滾(Rollback):

情境一

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

無,Not Support

  1. 調用者事務,被調用者無(Not Support)事務。
  2. 被調用者拋出異常,被調用者丟擲異常前的資料操作不受影響
    1. 調用者沒有捕獲異常,則調用者回滾
    2. 調用者捕獲異常,則調用者不受影響,回滾。

Case2-a:

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

<被調用者(callee)>

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

Case2-b:

<調用者>

@Transactional()
public void hasTransactional() {
    try{
        insertData();
        callee.hasTransactional();
    } catch(Exception e) {
    }
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

情境二

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

無,Not Support

  1. 調用者事務,被調用者無(Not Support)事務。
  2. 調用者拋出異常,調用者回滾;被調用者無事務,不受影響,不回滾。

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
    throw new RuntimeException();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void hasTransactional()
{
    insertData();
}

情境三

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

無,Not Support

  1. 調用者事務,被調用者無(Not Support)事務。
  2. 被調用者拋出異常,因調用者與被調用者皆無事務,所以調用者被調用者皆不會回滾。

<調用者>

public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

5. PROPAGATION_REQUIRED (支持當前 Transaction;如果不存在,則創建一個新的)

※在此傳播屬性下,被調用者是否新建 Transaction 取決於調用者是否帶著 Transaction 。

 

當被調用者函式使用 PROPAGATION_REQUIRED 此傳播類型時,用以下的情境進行說明 Transaction 是否會回滾(Rollback):

情境一

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,開啟事務

  1. 調用者事務,被調用者開啟事務。
  2. 被調用者拋出異常,被調用者回滾
    1. 調用者沒有捕獲異常,則調用者回滾
    2. 調用者捕獲異常並正常提交事務,則會發生Transaction silently rolled back because it has been marked as rollback-only的異常。

Case2-a:

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.REQUIRED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

Case2-b:

<調用者>

@Transactional()
public void hasTransactional() {
    try{
        insertData();
        callee.hasTransactional();
    } catch(Exception e) {
    }
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.REQUIRED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

情境二

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,開啟事務

  1. 調用者事務,被調用者開啟事務。
  2. 調用者拋出異常,因被調用者的事務與調用者同一個,所以調用者被調用者皆會回滾。

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
    throw new RuntimeException();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.REQUIRED)
public void hasTransactional() {
    insertData();
}

情境三

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,新建事務

  1. 調用者事務,被調用者新建事務。
  2. 被調用者拋出異常,因調用者事務,所以調用者回滾;被調用者回滾

<調用者>

public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.REQUIRED)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

6. PROPAGATION_REQUIRES_NEW (創建一個新 Transaction,如果 Transaction 存在,則暫停當前 Transaction)

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

 

當被調用者函式使用 PROPAGATION_REQUIRES_NEW 此傳播類型時,用以下的情境進行說明 Transaction 是否會回滾(Rollback):

情境一

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,新建事務

  1. 調用者事務,被調用者新建事務。
  2. 被調用者拋出異常被調用者回滾
    1. 調用者沒有捕獲異常,則調用者回滾
    2. 調用者捕獲異常,調用者不受影響,回滾。

Case2-a:

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

Case2-b:

<調用者>

@Transactional()
public void hasTransactional() {
    try{
        insertData();
        callee.hasTransactional();
    } catch(Exception e) {
    }
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

情境二

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,新建事務

  1. 調用者事務,被調用者新建事務。
  2. 調用者拋出異常,因被調用者事務與調用者事務與不是同一個,所以調用者回滾;被調用者不受影響,回滾。

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
    throw new RuntimeException();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void hasTransactional() {
    insertData();
}

情境三

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,新建事務

  1. 調用者事務,被調用者新建事務。
  2. 被調用者拋出異常,因調用者無事務,所以調用者回滾;被調用者回滾

<調用者>

public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

7. PROPAGATION_SUPPORTS (支持當前 Transaction;如果不存在,則以非 Transaction 方式執行)

※在此傳播屬性下,被調用者是否有 Transaction,完全依賴於調用者,調用者有 Transaction 則有 Transaction,調用者沒 Transaction 則沒 Transaction 。

 

當被調用者函式使用 PROPAGATION_SUPPORTS 此傳播類型時,用以下的情境進行說明 Transaction 是否會回滾(Rollback):

情境一

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,開啟事務

  1. 調用者事務,被調用者開啟事務。
  2. 被調用者拋出異常,被調用者回滾
    1. 調用者沒有捕獲異常,則調用者回滾
    2. 調用者捕獲異常並正常提交事務,則會發生 Transaction silently rolled back because it has been marked as rollback-only的異常。

Case2-a:

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.SUPPORTS)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

Case2-b:

<調用者>

@Transactional()
public void hasTransactional() {
    try{
        insertData();
        callee.hasTransactional();
    } catch(Exception e) {
    }
}

 

<被調用者(callee)>

@Transactional(propagation = Propagation.SUPPORTS)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

情境二

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

有,開啟事務

  1. 調用者事務,被調用者開啟事務。
  2. 調用者拋出異常,因被調用者的事務與調用者同一個,所以調用者被調用者皆會回滾。

<調用者>

@Transactional()
public void hasTransactional() {
    insertData();
    callee.hasTransactional();
    throw new RuntimeException();
}

<被調用者(callee)>

@Transactional(propagation = Propagation.SUPPORTS)
public void hasTransactional() {
    insertData();
}

情境三

調用者有事務

調用者拋出異常

被調用者有事務

被調用者拋出異常

  1. 調用者事務,被調用者事務。
  2. 被調用者拋出異常,因調用者與被調用者皆無事務,所以調用者被調用者皆不會回滾。

<調用者>

public void hasTransactional() {
    insertData();
    callee.hasTransactional();
}

<被調用者(callee)>

@Transactional(propagation = Propagation.SUPPORTS)
public void hasTransactional() {
    insertData();
    throw new RuntimeException();
}

 

//=======================================================

一篇文章的完成,總是要感謝先行者的付出,以下列出相關的參考資料
 
 
參考資料:

1) Spring傳播屬性有那麼難嗎?看這一篇就夠了
https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/673727/

2) 透徹的掌握 Spring 中@transactional 的使用
https://codertw.com/%E4%BC%BA%E6%9C%8D%E5%99%A8/145470/

3) Spring @Transactional註解淺談

https://iter01.com/61414.html

 

羅國榮
Ben Yao
2020/11/11 10:36:23

transaction很重要, 很好的參考文章