18個示例詳解 Spring 事務傳播機制(附測試原始碼)

小碼code發表於2022-01-18

什麼是事務傳播機制

事務的傳播機制,顧名思義就是多個事務方法之間呼叫,事務如何在這些方法之間傳播。

舉個例子,方法 A 是一個事務的方法,方法 A 執行的時候呼叫了方法 B,此時方法 B 有無事務以及是否需要事務都會對方法 A 和方法 B 產生不同的影響,而這個影響是由兩個方法的事務傳播機制決定的。

傳播屬性 Propagation 列舉

Spring 對事務的傳播機制在 Propagation 列舉中定義了7個分類:

  • REQUIRED 預設
  • SUPPORTS 支援
  • MANDATORY 強制
  • REQUIRES_NEW 新建
  • NOT_SUPPORTED 不支援
  • NEVER 從不
  • NESTED 巢狀

事務的傳播機制,是 spring 規定的。因為在開發中,最簡單的事務是,業務程式碼都處於同一個事務下,這也是預設的傳播機制,如果出現的報錯,所有的資料回滾。
但是在處理複雜的業務邏輯時,方法之間的呼叫,有以下的需求:

  • 呼叫的方法需要新增一個事務,新事務和原來的事務各自獨立。
  • 呼叫的方法不支援事務
  • 呼叫的方法是一個巢狀的事務

7種傳播機制詳解

首先建立兩個方法 A 和 B 實現資料的插入,插入資料A:

public class AService {
    public void A(String name) {
        userService.insertName("A-" + name);
    }

}

插入資料B:

public class BService {
    public void B(String name) {
        userService.insertName("B-" + name);
    }

}

使用虛擬碼建立 mainTest 方法和 childTest 方法

   public void mainTest(String name)  {
        // 存入a1
        A(a1);
        childTest(name);
    }

main 呼叫 test 方法,其中

   public void childTest(String name)  {
        // 存入b1
        B(b1);
       throw new RuntimeException(); 
       // 存入 b2
       B2(b2);
    }

以上虛擬碼,呼叫 mainTest 方法,如果mainTest 和childTest 都不使用事務的話,資料儲存的結果是如何呢?

因為都沒使用事務,所以 a1 和 b1 都存到成功了,而之後丟擲異常之後,b2是不會執行的。所以 a1 和 b1 都插入的資料,而 b2 沒有插入資料。

REQUIRED(預設事務)

      /**
	 * Support a current transaction, create a new one if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>This is the default setting of a transaction annotation.
	 */
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

如果當前不存在事務,就新建一個事務。如果存在事務,就加入到當前事務。這是一個預設的事務

示例1:根據場景舉個例子,在 childTest 新增事務,設定傳播屬性為 REQUIRED,虛擬碼如下:

   public void mainTest(String name)  {
        // 存入a1
        A(a1);
        childTest(name);
    }
   @Transactional(propagation = Propagation.REQUIRED)
   public void childTest(String name)  {
        // 存入b1
        B(b1);
       throw new RuntimeException(); 
    }

因為 mainTest 沒有事務,而 childTest 又是新建一個事務,所以 a1 新增成功。在 childTest 因為丟擲了異常,不會執行 b2 新增,而 b1 新增回滾。最終 a1 新增成功,b1沒新增成功。

示例2:在 mainTest 和 childTest 都新增事務,傳播屬性都為 REQUIRED,虛擬碼如下:

   @Transactional(propagation = Propagation.REQUIRED)
   public void mainTest(String name) {
        // 存入a1
        A(a1);
        childTest(name);
    }
   @Transactional(propagation = Propagation.REQUIRED)
   public void childTest(String name) {
        // 存入b1
        B(b1);
       throw new RuntimeException(); 
   }

根據 REQUIRED 傳播屬性,如果存在事務,就加入到當前事務。兩個方法都屬於同一個事務,同一個事務的話,如果有發生異常,則全部都回滾。所以 a1 和 b1 都沒新增成功。

SUPPORTS

/**
	 * Support a current transaction, execute non-transactionally if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>Note: For transaction managers with transaction synchronization,
	 */

如果當前沒有事務,則以非事務的方式執行。如果存在事務,就加入到當前事務。

示例3:childTest 新增事務,傳播屬性設定為 SUPPORTS,虛擬碼如下:

   public void mainTest(String name) {
        A(a1);
        childTest(name);
    }
   @Transactional(propagation = Propagation.SUPPORTS)
   public void childTest(String name) {
        B(b1);
       throw new RuntimeException(); 
    }

傳播屬性為 SUPPORTS,如果沒有事務,就以非事務的方式執行。表明兩個方法都沒有使用事務,沒有事務的話,a1、b1 都新增成功。

示例4:mainTest 新增事務,設定傳播屬性為 REQUIRED。childTest 新增事務,設定傳播屬性為 SUPPORTS,虛擬碼如下:

   @Transactional(propagation = Propagation.REQUIRED)
   public void mainTest(String name) {
        A(a1);
        childTest(name);
    }
   @Transactional(propagation = Propagation.SUPPORTS)
   public void childTest(String name) {
        B(b1);
       throw new RuntimeException(); 
       B2(b2);
    }

SUPPORTS 傳播屬性,如果存在事務,就加入到當前事務。mainTest 和 childTest 都屬於同一個事務,而 childTest 丟擲異常,a1 和b1 新增都回滾,最終 a1、b1 新增失敗。

MANDATORY

/**
	 * Support a current transaction, throw an exception if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */

如果存在事務,就加入到當前事務。如果不存在事務,就報錯
這就說明如果想呼叫 MANDATORY 傳播屬性的方法,一定要有事務,不然就會報錯。

MANDATORY 類似功能限制,必須要被有事務的方法的呼叫,不然就會報錯。

示例5: 首先在 childTest 新增事務,設定傳播屬性為 MANDATORY,虛擬碼如下:

  public void mainTest(String name) {
       A(a1);
       childTest(name);
   }
  @Transactional(propagation = Propagation.MANDATORY)
  public void childTest(String name) {
       B(b1);
      throw new RuntimeException(); 
      B2(b2);
   }

在控制檯直接報錯:

No existing transaction found for transaction marked with propagation 'mandatory'

說明被標記為 mandatory 傳播屬性沒找到事務,直接報錯。因為 mainTest 沒有事務,a1 新增成功。而 childTest 由於報錯,b1 新增失敗。

示例6: mainTest 新增事務,設定傳播屬性為 REQUIRED。childTest 新增事務,設定傳播屬性為 MANDATOR,虛擬碼如下:

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
       childTest(name);
   }
  @Transactional(propagation = Propagation.MANDATORY)
  public void childTest(String name) {
       B(b1);
      throw new RuntimeException(); 
   }

如果存在事務,就把事務加入到當前事務。同一個事務中 childTest 丟擲異常,a1 和 b1 新增被回滾,所以a1 和 b1新增失敗。

REQUIRES_NEW

   /**
	 * Create a new transaction, and suspend the current transaction if one exists.
	 * Analogous to the EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 */

建立一個新的事務。如果存在事務,就將事務掛起。

無論是否有事務,都會建立一個新的事務。

示例7:childTest 新增事務,設定傳播屬性為 REQUIRES_NEW,虛擬碼如下:

  public void mainTest(String name) {
       A(a1);
       childTest(name);
   }
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void childTest(String name) {
       B(b1);
      throw new RuntimeException(); 
   }

mainTest 不存在事務,a1 新增成功,childTest 新建了一個事務,報錯,回滾 b1。所以 a1 新增成功,b1 新增失敗。

示例8:mainTest 新增事務,設定傳播屬性為 REQUIRED。childTest 新增事務,設定傳播屬性為 REQUIRES_NEW,虛擬碼如下:

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
       childTest(name);
   }
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void childTest(String name) {
       B(b1);
      throw new RuntimeException(); 
   }

mainTest 建立了一個事務,childTest 新建一個事務,在 childTest 事務中,丟擲異常,b1 回滾,異常拋到 mainTest 方法,a1 也回滾,最終 a1 和 b1 都回滾。

示例9:在示例8中,如果不想讓 REQUIRES_NEW 傳播屬性影響到被呼叫事務,將異常捕獲就不會影響到被呼叫事務。

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
       try {
              childTest(name);
       } catch (Exception e) {
           e.printStackTrace();
       }   
   }
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void childTest(String name) {
       B(b1);
      throw new RuntimeException(); 
   }

childTest 丟擲了異常,在 mainTest 捕獲了,對 mainTest 沒有影響,所以 b1 被回滾,b1 新增失敗,a1 新增成功。

示例10:mainTest 設定傳播屬性為 REQUIRED,並在 mainTest 丟擲異常。childTest 同樣設定 REQUIRES_NEW 傳播屬性,虛擬碼如下:

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
       childTest(name);
       throw new RuntimeException();    
   }
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void childTest(String name) {
       B(b1);  
       B2(b2);
   }

childTest 是一個新建的事務,只要不丟擲異常是不會回滾,所以 b1 新增成功,而 mainTest 丟擲了異常,a1 新增失敗。

REQUIRES_NEW 傳播屬性如果有異常,只會從被呼叫方影響呼叫方,而呼叫方不會影響呼叫方,即 childTest 丟擲異常會影響 mainTest,而 mainTest 丟擲異常不會到 childTest。

NOT_SUPPORTED

  /**
	 * Execute non-transactionally, suspend the current transaction if one exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 */

無論是否存在當前事務,都是以非事務的方式執行

示例11:childTest 新增事務,設定傳播屬性為 NOT_SUPPORTED,虛擬碼如下:

  public void mainTest(String name) {
       A(a1);
       childTest(name);
          
   }
  @Transactional(propagation = Propagation.NOT_SUPPORTED)
  public void childTest(String name) {
       B(b1);  
      throw new RuntimeException();  
   }

NOT_SUPPORTED 都是以非事務的方式執行,childTest 不存在事務,b1 新增成功。而 mainTest 也是沒有事務,a1 也新增成功。

示例12:childTest 新增事務,設定傳播屬性為 NOT_SUPPORTED,mainTest 新增預設傳播屬性 REQUIRED,虛擬碼如下:

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
       childTest(name);
          
   }
  @Transactional(propagation = Propagation.NOT_SUPPORTED)
  public void childTest(String name) {
       B(b1);  
      throw new RuntimeException();  
    }

其中 childTest 都是以非事務的方式執行,b1 新增成功。而 mainTest 存在事務,報錯後回滾,a1 新增失敗。

NEVER

   /**
	 * Execute non-transactionally, throw an exception if a transaction exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */

不使用事務,如果存在事務,就丟擲異常

NEVER 的方法不使用事務,呼叫 NEVER 方法如果有事務,就丟擲異常。

示例13:childTest 新增 NEVER 傳播屬性,虛擬碼如下:

  public void mainTest(String name) {
       A(a1);
       childTest(name);
          
   }
  @Transactional(propagation = Propagation.NEVER)
  public void childTest(String name) {
       B(b1);  
      throw new RuntimeException();  
      B2(b2);
   }

NEVER 不使用事務,mainTest 也不使用事務,所以 a1 和 b1 都新增成功,b2新增失敗。

示例14: mainTest 新增 REQUIRED 傳播屬性,childTest 傳播屬性設定為 NEVER,虛擬碼如下:

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
       childTest(name);
          
   }
  @Transactional(propagation = Propagation.NEVER)
  public void childTest(String name) {
       B(b1);  
      throw new RuntimeException();  
   }

mainTest 存在事務,導致 childTest 報錯,b1新增失敗,childTest 拋錯到 mainTest,a1 新增失敗。

NESTED

/**
	 * Execute within a nested transaction if a current transaction exists,
	 * behave like PROPAGATION_REQUIRED else. There is no analogous feature in EJB.
	 * <p>Note: Actual creation of a nested transaction will only work on specific
	 */

如果當前事務存在,就執行一個巢狀事務。如果不存在事務,就和 REQUIRED 一樣新建一個事務。

示例15: childTest 設定 NESTED 傳播屬性,虛擬碼如下:

  public void mainTest(String name) {
       A(a1);
       childTest(name);
          
   }
  @Transactional(propagation = Propagation.NESTED)
  public void childTest(String name) {
       B(b1);  
      throw new RuntimeException();  
   }

在 childTest 設定 NESTED 傳播屬性,相當於新建一個事務,所以 b1 新增失敗, mainTest 沒有事務,a1 新增成功。

示例16:設定 mainTest 傳播屬性為 REQUIRED,新建一個事務,並在方法最後丟擲異常。 childTest 設定屬性為 NESTED,虛擬碼如下:

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
       childTest(name);
       throw new RuntimeException();     
   }
  @Transactional(propagation = Propagation.NESTED)
  public void childTest(String name) {
       B(b1);         
      B2(b2);
   }

childTest 是一個巢狀的事務,當主事務的丟擲異常時,巢狀事務也受影響,即 a1、b1 和 b2 都新增失敗。和示例10不同的是,示例10不會影響 childTest 事務。

  • NESTED 和 REQUIRED_NEW 的區別:
    • REQUIRED_NEW 是開啟一個新的事務,和呼叫的事務無關。呼叫方回滾,不會影響到 REQUIRED_NEW 事務。
    • NESTED 是一個巢狀事務,是呼叫方的一個子事務,如果呼叫方事務回滾,NESTED 也會回滾。

示例17:和示例16一樣,在 mainTest 設定傳播屬性為 REQUIRED,childTest 設定傳播屬性為 NESTED,不同的是,在 mainTest 捕獲 childTest 丟擲的異常,虛擬碼如下:

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
      try {
           childTest(name);
       } catch (RuntimeException e) {
           e.printStackTrace();
       } 
   }
  @Transactional(propagation = Propagation.NESTED)
  public void childTest(String name) {
      B(b1);         
      B2(b2);
      throw new RuntimeException(); 
   }

childTest 是一個子事務,報錯回滾,b1 和 b2 都新增失敗。而 mainTest 捕獲了異常,不受異常影響,a1 新增成功。

示例18:將示例2改造一下,mainTest 捕獲 childTest 丟擲的異常,虛擬碼如下:

  @Transactional(propagation = Propagation.REQUIRED)
  public void mainTest(String name) {
       A(a1);
      try {
           childTest(name);
       } catch (RuntimeException e) {
           e.printStackTrace();
       } 
   }
  @Transactional(propagation = Propagation.REQUIRED)
  public void childTest(String name) {
      B(b1);         
      B2(b2);
      throw new RuntimeException(); 
   }

mainTest 和 childTest 兩個方法都處於同一個事務,如果有一個方法報錯回滾,並且被捕獲。整個事務如果還有資料新增就會丟擲 Transaction rolled back because it has been marked as rollback-only 異常,同一個事務,不能出現有的回滾了,有的不回滾,要麼一起回滾,要不一起執行成功。所以全部資料都新增失敗。

  • 對比示例17示例18,NESTED 和 REQUIRED 的區別:
    • REQUIRED 傳播屬性表明呼叫方和被呼叫方都是使用同一個事務,被呼叫方出現異常,無論異常是否被捕獲,因為屬於同一個事務,只要發生異常,事務都會回滾。
    • NESTED 被呼叫方出現異常,只要異常被捕獲,只有被呼叫方事務回滾,呼叫方事務不受影響。

總結

傳播屬性 總結
REQUIRED 預設屬性,所有的事務都處於同一個事務下,出現異常,不管是否捕獲所有事務回滾
SUPPORTS 如果不存事務,就以非事務的方式執行,存在事務就加入該事務
MANDATORY 強制呼叫方新增事務,如果不存在事務就報錯,存在事務就加入該事務
REQUIRES_NEW 無論呼叫方是否存在事務,都會建立新的事務,並且呼叫方異常不會影響 REQUIRES_NEW事務
NOT_SUPPORTED 無論是否呼叫方是否存在事務,都是以非事務的方式執行,出現異常也會回滾
NEVER 不用事務,存在事務就報錯,和 MANDATORY 相反
NESTED 巢狀事務,新建一個子事務,事務執行相互獨立,如果呼叫方出現異常,直接回滾

測試原始碼

參考

如果覺得文章對你有幫助的話,請點個推薦吧!

相關文章