Spring事務傳播機制的理解與代碼驗證

環境:

      Springboot:2.2.3.RELEASE

      Spring:5.2.3.RELEASE

事務傳播行爲枚舉參見Spring源碼:org.springframework.transaction.annotation.Propagation

先來了解一下Spring事務的傳播機制(由於英語不好,直接google翻譯了)

簡單終結一下:

 

枚舉 翻譯
REQUIRED
支持當前事務,如果不存在則創建新事務(默認) 
SUPPORTS 支持當前事務,如果不存在則非事務執行
MANDATORY 支持當前事務,如果不存在則拋出異常。
REQUIRES_NEW 創建一個新事務,並暫停當前事務(如果存在)
NOT_SUPPORTED 非事務執行,如果存在當前事務,則掛起當前事務
NEVER 非事務執行,如果存在事務,則引發異常
NESTED 如果當前事務存在,則在嵌套事務中執行

接下來進行代碼驗證(以下的代碼實例會最終打包放到付件中)

準備工作:

    數據庫(每次驗證前均已清空數據庫):

create table demo
(
	id int auto_increment
		primary key,
	uid varchar(30) not null,
	remark varchar(30) null,
	inputdate datetime not null,
	constraint demo_uid_uindex
		unique (uid)
);

 請求路由

@RequestMapping("/test/{trans}")
public String test(@PathVariable("trans") String trans){
    if("never".equals(trans)){
        transService.neverTrans();
    }else if("mandatory".equals(trans)){
        transService.mandatoryTrans();
    }else if("required".equals(trans)){
        transService.requiredTrans();
    }else if("trans".equals(trans)){
        transService.trans();
    }else if("supports".equals(trans)){
        transService.supportsTrans();
    }else if("supportswithtrans".equals(trans)){
        transService.supportsWithTrans();
    }else if("requeirednew".equals(trans)){
        transService.requiredNewTrans();
    }else if("notsupported".equals(trans)){
        transService.notsupportedTrans();
    }else if("netsted".equals(trans)){
        transService.nestedTrans();
    }else if("netstedwithserviceerror".equals(trans)){
        transService.nestedWithTransServiceError();
    }else if("netstedwithdaoerror".equals(trans)){
        transService.nestedWithTransDaoError();
    }
    return "ok";
}

Mapper.xml

<insert id="insertDemoWithMandatory">
  insert into demo (uid, remark, inputdate) value (#{uid,jdbcType=VARCHAR},#{remark,jdbcType=VARCHAR},CURRENT_TIMESTAMP);
</insert>
<insert id="insertDemoWithNever">
  insert into demo (uid, remark, inputdate) value (#{uid,jdbcType=VARCHAR},#{remark,jdbcType=VARCHAR},CURRENT_TIMESTAMP);
</insert>
<insert id="insertDemo">
  insert into demo (uid, remark, inputdate) value (#{uid,jdbcType=VARCHAR},#{remark,jdbcType=VARCHAR},CURRENT_TIMESTAMP);
</insert>
<insert id="insertDemoError">
  insert into demo (uid, remark, inputdate) value (#{uid1,jdbcType=VARCHAR},#{remark,jdbcType=VARCHAR},CURRENT_TIMESTAMP);
</insert>
<insert id="insertDemoWithSupportnew">
  insert into demo (uid, remark, inputdate) value (#{uid,jdbcType=VARCHAR},#{remark,jdbcType=VARCHAR},CURRENT_TIMESTAMP);
</insert>
<insert id="insertDemoWithNested">
   insert into demo (uid, remark, inputdate) value (#{uid,jdbcType=VARCHAR},#{remark,jdbcType=VARCHAR},CURRENT_TIMESTAMP);
</insert>
<insert id="insertDemoWithNestedOnError">
  insert into demo (uid, remark, inputdate) value (#{uid1,jdbcType=VARCHAR},#{remark,jdbcType=VARCHAR},CURRENT_TIMESTAMP);
</insert>

DAO:

@Transactional(propagation = Propagation.NEVER)
int insertDemoWithNever(@Param("uid")String uid,@Param("remark") String remark);
@Transactional(propagation = Propagation.MANDATORY)
int insertDemoWithMandatory(@Param("uid")String uid, @Param("remark") String remark);
@Transactional(propagation = Propagation.REQUIRES_NEW)
int insertDemoWithSupportnew(@Param("uid")String uid, @Param("remark") String remark);
int insertDemo(@Param("uid")String uid, @Param("remark") String remark);
int insertDemoError(@Param("uid")String uid, @Param("remark") String remark);
@Transactional(propagation = Propagation.NESTED)
int insertDemoWithNested(@Param("uid")String uid, @Param("remark") String remark);
@Transactional(propagation = Propagation.NESTED)
int insertDemoWithNestedOnError(@Param("uid")String uid, @Param("remark") String remark);

 

  • 0. NONE 沒有事務

    驗證思路:準備兩條入庫的SQL,其中一條報錯,因爲沒有事務,會導致數據不一致性

    路徑:/test/trans

   TransService:

/**
* 不帶事務的 遇到錯誤不會回滾,會導致數據不一致(部分寫入成功,部分寫入失敗)
* @return
*/
@Override
public boolean trans() {
  demoMapper.insertDemo("uid1","transService");
  demoMapper.insertDemoError("uid2","transService1");
  return false;
}

分析:

   demoMapper.insertDemoError("uid2","transService1");  因SQL中賦值的時候有錯誤,導致綁定參數的時候異常,但是因爲這兩個方法均沒有開啓事務,異常發生時,demoMapper.insertDemo("uid1","transService"); 已經執行完畢,導致數據不一致。

執行完畢後數據庫:

總結:沒有事務容易造成數據的不一致性。

  • 1. REQUIRED 支持當前事務,如果不存在則創建新事務

   驗證思路:準備兩條入庫的SQL,其中一條報錯,開啓事務,報錯會導致事物回滾

   路徑:/test/required

   TransService:

/**
* 該方法在事務裏執行,如果當前沒有事務開啓一個事務(默認沒有事務,見this.trans())
* @return
*/
@Override
@Transactional(propagation =  Propagation.REQUIRED)
public boolean requiredTrans() {
  demoMapper.insertDemo("uid1","transService");
  demoMapper.insertDemoError("uid2","transService1");
  return false;
}

   分析:

  與沒有事務相比,該方法前聲明瞭事務傳播行爲REQUIRED,執行到  demoMapper.insertDemoError("uid2","transService1");時報錯,導致事務回滾,數據庫數據庫沒有保存任何信息

   總結:REQUIRED會開啓事務,且發生異常時,事務會進行回滾,數據一致性的到保證。

執行完畢後數據庫:

  • 2. SUPPORTS 支持當前事務,如果不存在則非事務執行

   驗證思路:分別在有/沒有事務的方法裏,調用另一個事務傳播行爲SUPPORTS的方法

   路徑

        1. 當前沒有事務:/test/supports

        2. 當前有事務:/test/supportswithtrans

   TransService:

        

/**
* 當前沒有事物,demoService.supportsTrans()的也不會有事務,會插入一條數據,造成數據不一致
* @return
*/
@Override
public boolean supportsTrans() {
  demoService.supportsTrans();
  return false;
}
/**
* 當前有事物,demoService.supportsTrans()的也會有事務,不會造成數據不一致
* @return
*/
@Transactional(propagation = Propagation.REQUIRED)
@Override
public boolean supportsWithTrans() {
  demoService.supportsTrans();
  return false;
}

   DemoService

@Override
@Transactional(propagation = Propagation.SUPPORTS)
public boolean supportsTrans() {
    demoMapper.insertDemo("uid1","demoService");
    demoMapper.insertDemoError("uid2","demoService1");
    return false;
}

分析:

   supportsTrans()未聲明事務,demoService.supportsTrans();以無事務的行爲執行,Mapper執行時遇到錯誤不會回滾。

   supportsWithTrans()聲明事務,demoService.supportsTrans();以有事務的行爲執行,Mapper執行時遇到錯誤會回滾。

   總結:

SUPPORTS 不會主動創建事務,是否支持事務來自於其調用方是否開啓了事務。

執行完畢後數據庫:

 

 

  • 3. MANDATORY 支持當前事務,如果不存在則拋出異常

   驗證思路:分別在沒有事務的方法裏,調用另一個事務傳播行爲MANDATORY的方法

   路徑:/test/mandatory

   TransService:

/**
 * 當前方法未開啓事務,調用的demoMapper.insertDemoWithMandatory(String,String) 必須在事務中執行,會報錯
 * @return
 */
@Override
public boolean mandatoryTrans() {
    System.out.println("service[none trans],dao[propagration=mandatory]");
    demoMapper.insertDemoWithMandatory("uid","mandatorytest");
    return false;
}

   分析:

   mandatoryTrans()未聲明事務,demoMapper.insertDemoWithMandatory();聲明瞭行爲爲MANDATORY的事務,insertDemoWithMandatory會因爲沒有事務而報錯

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

   總結:

MANDATORY不會主動創建事務,但是其要求當前必須有事務

執行完畢後數據庫:

    未觸發

  • 4. REQUIRES_NEW 創建一個新事務,並暫停當前事務(如果存在)

   驗證思路:在有事務的方法裏,調用另一個事務傳播行爲REQUIRED_NEW的方法,且在執行完後執行一條錯誤的代碼,導致當前事務回滾

   路徑:/test/requeirednew

   TransService:

/**
 * 當前有事務,demoMapper.insertDemoWithSupportnew(String,String)開始了自己的事務,
 * 當前事務回滾不會造成demoMapper.insertDemoWithRequiresNew事務的回滾
 * @return
 */
@Override
@Transactional(propagation = Propagation.REQUIRED)
public boolean requiredNewTrans() {
    demoMapper.insertDemo("uid1","transService1");
    demoMapper.insertDemoWithRequiresNew("uid2","transService1");
    demoMapper.insertDemoError("uid3","transService1");
    return false;
}

   分析:

     執行完畢demoMapper.insertDemoWithRequiresNew("uid2","transService1");後繼續執行requiredNewTrans()引發異常,會導致requiredNewTrans()的事務回滾,但並不影響insertDemoWithRequiresNew的事務,insertDemoWithRequiresNew會正常執行

   總結:

    REQUIRES_NEW開啓了的新事務並不是加入到當前的事務中來,當前事務的異常並不會引發REQUIRES_NEW方法的事務回滾。

執行完畢後數據庫:

  • 5. NOT_SUPPORTED 非事務執行,如果存在當前事務,則掛起當前事務

   驗證思路:在有事務的方法裏,調用另一個事務傳播行爲NOT_SUPPORTED的方法, 且NOT_SUPPORTED的方法會執行異常,但會被捕獲並處理。

   路徑:/test/notsupported

   TransService:

/**
*   當前有事務,demoMapper.insertDemoWithSupportnew(String,String)開始了自己的事務,
*      * 當前事務回滾不會造成demoMapper.insertDemoWithSupportnew事務的回滾
* @return
*/
@Override
@Transactional(propagation = Propagation.REQUIRED)
public boolean notsupportedTrans() {
  demoMapper.insertDemo("uid1","transService1");
  try {
      demoService.insertDemoWithnotSupported();
  }catch (Exception e){
      System.out.println(" demoService.insertDemoWithnotSupported執行報錯,其數據不一致");
  }
  demoMapper.insertDemo("uid2","transService1");
  return false;
}

   DemoService

@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public boolean insertDemoWithnotSupported() {
  demoMapper.insertDemo("uid1-1","demoService");
  demoMapper.insertDemoError("uid2-1","demoService1");
  return false;
}

分析:

   notsupportedTrans()執行過程中未發生異常,事務正常提交,insertDemoWithnotSupported執行到demoMapper.insertDemoError("uid2-1","demoService1");會發生異常,當前DemoService沒有事務,第一條會正常插入,數據庫中應該爲三條(2+1)。

   總結:

NOT_SUPPORTED對當前的事務做了一個掛起後執行自己的部分,並且自己這部分時以無事務的形式執行,其異常並不會回滾,會導致數據不一致。NOT_SUPPORTED並不會影響掛起的事務的繼續執行及提交或回滾。

執行完畢後數據庫:

  • 6. NEVER 非事務執行,如果存在事務,則引發異常

   驗證思路:在有事務的方法裏,調用另一個事務傳播行爲NEVER的方法

   路徑:/test/never

   TransService:

/**
* 當前方法開啓事務,調用的demoMapper.insertDemoWithNever(String,String) 不允許事務,會報錯
* @return
*/
@Override
@Transactional(propagation = Propagation.REQUIRED)
public boolean neverTrans() {
  System.out.println("service[propagation=requeired],dao[propagration=never]");
  demoMapper.insertDemoWithNever("uid","nevertest");
  return false;
}

   分析:

neverTrans()開啓了當前事務,調用不支持的事務方法時,觸發了異常:

IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

   總結:

   NEVER並不會掛起當前事務,而是在當前存在事務時直接異常,強制無事務。

執行完畢後數據庫:

    未觸發

  • 7. NESTED 如果當前事務存在,則在嵌套事務中執行

   驗證思路:step1. 在無事務的方法裏,調用另一個事務傳播行爲NESTED的方法,同時NESTED的方法會報錯,以此來檢查嵌套事務是否會影響無事務方法的開啓。

                         step2. 在有事務的方法裏,調用另一個事務傳播行爲NESTED的方法,同時NESTED的方法會報錯,以此來檢查嵌套事務是否會影響當前事務方法的提交。

                        step3. 在有事務的方法裏,調用另一個事務傳播行爲NESTED的方法,當前事務會報錯,以此來檢查是否會影響嵌套事務提交。

   路徑

       1.當前無事務 /test/netsted

       2. 當前有事務,嵌套事務異常:/test/netstedwithdaoerror

      3. 當前有事務,且有異常:/test/netstedwithserviceerror

   TransService:

/**
 * 當前沒有事務,demoService.insertDemoWithNested會開啓事務,其失敗不會造成其本身的數據不一致,
 * 但不會影響當前事務的特性(當前異常會因爲沒有事務而引發數據不一致)
 * @return
 */
@Override
public boolean nestedTrans() {
    demoMapper.insertDemo("uid1","transService1");
    try {
        demoService.insertDemoWithNested();
    }catch (Exception e){
        System.out.println(" demoService.insertDemoWithNested,其數據一致");
    }
    demoMapper.insertDemoError("uid2","transService1");
    return false;
}
/**
 * 當前有事務,demoMapper.insertDemoWithNested會開啓嵌套事務,其事務的提交和回滾依賴當前事務,
 * 當前事務失敗,其事務不會被提交
 * @return
 */
@Override
@Transactional(propagation = Propagation.REQUIRED)
public boolean nestedWithTransServiceError() {
    demoMapper.insertDemo("uid1","transService1");
    try {
        demoMapper.insertDemoWithNested("uid-0","transService");
    }catch (Exception e){
        System.out.println(" demoService.insertDemoWithNested,其數據一致");
    }
    demoMapper.insertDemoError("uid2","transService1");
    return false;
}
/**
 * 當前有事務,demoMapper.insertDemoWithNested會開啓嵌套事務,其事務的提交和回滾依賴當前事務,
 * 當前事務成功,其事務將被提交,但其失敗不會影響當前事務
 * @return
 */
@Override
@Transactional(propagation = Propagation.REQUIRED)
public boolean nestedWithTransDaoError() {
    demoMapper.insertDemo("uid1","transService1");
    try {
        demoMapper.insertDemoWithNestedOnError("uid-0","transService");
    }catch (Exception e){
        System.out.println(" demoService.insertDemoWithNestedOnError");
    }
    return false;
}

   DemoService

@Override
@Transactional(propagation = Propagation.NESTED)
public boolean insertDemoWithNested() {
    demoMapper.insertDemo("uid1-1","demoService");
    demoMapper.insertDemoError("uid2-1","demoService1");
    return false;
}

 

分析:

當前無事務的情況:嵌套事務遇到錯誤時是回滾,對當前調用方沒有影響。

當前有事務,嵌套事務異常情況:當前事務正常提交,嵌套事務因爲異常並未成功保存數據。

當前有事務有異常,嵌套事務無異常情況:嵌套事務未提交成功,當前是一回滾。

   總結:

通過三組對比發現,嵌套事務的在當前事務失敗的時候不會進行提交,但當前正常提交時,嵌套事務是能正常提交和回滾的,也就是嵌套事務的提交取決於當前事務的執行情況。

特別注意:NESTED傳播行爲在有些地方解釋爲不通的廠商實現機制不一樣,用之前需要再充分了解一下,從Spring的源碼上看,也確實如此。

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