Spring 事務管理及失效總結

Spring 事務管理及失效總結

所謂事務管理,其實就是“按照給定的事務規則來執行提交或者回滾操作”。

Spring 並不直接管理事務,而是提供了多種事務管理器,他們將事務管理的職責委託給 Hibernate 或者 JTA 等持久化機制所提供的相關平臺框架的事務來實現。

Spring 事務管理器接口: org.springframework.transaction.PlatformTransactionManager ,通過這個接口,Spring 爲各個平臺如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager) 等都提供了對應的事務管理器,但是具體的實現就是各個平臺自己的事情了。

Spring 事務的分類

Spring 提供了兩種事務管理方式:聲明式事務管理編程式事務管理。對不同的持久層訪問技術,編程式事務提供一致的事務編程風格,通過模板化的操作一致性地管理事務;而聲明式事務基於 Spring AOP 實現,卻並不需要程序開發者成爲 AOP 專家,亦可輕易使用 Spring 的聲明式事務管理。

聲明式事務

Spring 的聲明式事務管理是建立在 Spring AOP 機制之上的,其本質是對目標方法前後進行攔截,並在目標方法開始之前創建或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。

簡單地說,聲明式事務是編程式事務 + AOP 技術包裝,使用註解進行掃包,指定範圍進行事務管理。聲明式事務管理要優於編程式事務管理,這正是 Spring 倡導的非侵入式的開發方式。

示例:

@Transactional
public void transactionDemo {
  // TODO 業務代碼
}

編程式事務

在 Spring 出現以前,編程式事務管理對基於 POJO 的應用來說是唯一選擇。我們需要在代碼中顯式調用 beginTransaction()、commit()、rollback() 等事務管理相關的方法,這就是編程式事務管理。

簡單地說,編程式事務就是在代碼中顯式調用開啓事務、提交事務、回滾事務的相關方法,因此代碼侵入性較大。

示例:

@Autowired
private PlatformTransactionManager transactionManager;

public void transactionDemo() {
    TransactionStatus transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
          // TODO 業務代碼
          
          // 提交事務
          this.transactionManager.commit(transactionStatus);
    } catch (Exception e) {
          // 回滾事務
          this.transactionManager.rollback(transactionStatus);
    }
}

Spring 事務的原理

使用 AOP 環繞通知異常通知

注意: 在使用 Spring 事務時不能使用 try-catch 進行異常捕獲,要將異常拋給外層,使其進行異常攔截,觸發事務機制。

事務的傳播行爲

所謂事務的傳播行爲是指如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行爲。事務傳播行爲是爲了解決業務層方法之間互相調用的事務問題。

當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啓一個新事務,並在自己的事務中運行。

在 Spring 中有七種事務傳播行爲, 下面我們就來看看吧。

REQUIRED

如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。@Transactional 註解默認使用就是這個事務傳播行爲。
也就是說:

  • 如果外部方法沒有開啓事務的話,REQUIRED 修飾的內部方法會新開啓自己的事務,且開啓的事務相互獨立,互不干擾。
  • 如果外部方法開啓事務並且被 REQUIRED 的話,所有 REQUIRED 修飾的內部方法和外部方法均屬於同一事務,只要一個方法回滾,整個事務均回滾。

示例:

/**
 * @author star
 */
public class ClassA {

    @Transactional(propagation = Propagation.REQUIRED)
    public void methoA() {
       // TODO 業務代碼
        ClassB classB = new ClassB();
        classB.methodB();
    }
}

/**
 * @author star
 */
public class ClassB {

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodB() {
        // TODO 業務代碼
    }
}

使用 REQUIRED 傳播行爲修飾的 methodA() 和 methodB() 的話,兩者使用的就是同一個事務,只要其中一個方法發生回滾,整個事務都回滾。

REQUIRES_NEW

創建一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說,不管外部方法是否開啓事務,REQUIRES_NEW 修飾的內部方法會開啓一個新的事務。如果外部方法開啓事務,則兩個事務互不干擾,相互獨立,並且外部事務會掛起,等待內部事務執行完後,才繼續執行。

示列:


/**
 * @author star
 */
public class ClassA {

    @Transactional(propagation = Propagation.REQUIRED)
    public void methoA() {
       // TODO 業務代碼
        ClassB classB = new ClassB();
        classB.methodB();
    }
}

/**
 * @author star
 */
public class ClassB {

     @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // TODO 業務代碼
    }
}

如果使用 REQUIRED 事務傳播行爲修飾 methodA(),使用 REQUIRES_NEW 修飾 methodB()。那麼,methodA() 發生異常回滾,methodB() 是不會跟着回滾,因爲 methodB() 開啓了獨立的事務。但是,如果 methodB() 發生異常回滾了,並且拋出的異常被 methodA() 的事務管理機制檢測到了,methodA() 也會回滾。

SUPPORTS

如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。這個通常是用來處理那些並非原子性的非核心業務邏輯操作。不常用。

NOT_SUPPORTED

以非事務方式運行,如果當前存在事務,則把當前事務掛起。它可以幫助將事務極可能的縮小,因爲一個事務越大,它存在的風險也就越多,所以在處理事務的過程中,要保證儘可能的縮小範圍。

示列:

/**
 * @author star
 */
public class ClassA {

    @Transactional(propagation = Propagation.REQUIRED)
    public void methoA() {
       // TODO 業務代碼
        ClassB classB = new ClassB();
        classB.methodB();
    }
}

/**
 * @author star
 */
public class ClassB {

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void methodB() {
        // TODO 執行 1000 次非核心業務邏輯
    }
}

假如 methodB() 執行循環 1000 次的非核心業務邏輯操作,並且它處在 methodA() 的事務中,這樣會造成事務太大,導致出現一些難以考慮周全的異常情況。所以,使用 NOT_SUPPORTED 修飾 methodB(),當執行到 methodB() 時,將 methodA() 的事務掛起,等 methodB() 以非事務的狀態運行完成後,再繼續 methodA() 的事務。

NEVER

以非事務方式運行,如果當前存在事務,則拋出拋出Runtime 異常,強制停止執行。

如果 methodA() 是使用 REQUIRED 修飾的, 而methodB() 的是使用 NEVER 修飾的。當執行到 methodB() 時,就要拋出異常了。

MANDATORY

如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。也就是說,MANDATORY 要求上下文中必須要存在事務,否則就會拋出異常。

配置 MANDATORY 級別的事務是有效控制上下文調用代碼而遺漏添加事務管理的保證手段。比如,一段代碼不能單獨被調用執行,但是一旦被調用,就必須有事務包含的情況,就可以使用 MANDATORY 級別的事務。

NESTED

如果當前存在事務,則創建一個事務作爲當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於 REQUIRED

也就是說,如果外部方法開啓事務的話,NESTED 修飾的內部方法屬於外部事務的子事務,外部主事務回滾的話,子事務也會回滾,而內部子事務可以單獨回滾而不影響外部主事務和其他子事務。因爲 NESTED 事務它有一個 savepoint,在內部方法執行失敗後進行回滾,外部方法也會回滾到 savepoint 點上。此時,如果外部方法將內部方法拋出的異常進行了捕獲則會繼續往下執行直到完成自己的事務。如果外部方法沒有捕獲異常,則會根據事務規則進行回滾。

如果外部方法未開啓事務,NESTED 和 REQUIRED 作用相同,修飾的內部方法都會新開啓自己的事務,且開啓的事務相互獨立,互不干擾。

示列:

/**
 * @author star
 */
public class ClassA {

    @Transactional(propagation = Propagation.REQUIRED)
    public void methoA() {
        // TODO 業務代碼
        ClassB classB = new ClassB();
        try {
			// savepoint
			classB.methodB();
		} catch (Exception e) {
			// TODO 執行其他業務
		}
        // TODO 業務代碼
    }
}

/**
 * @author star
 */
public class ClassB {

    @Transactional(propagation = Propagation.NESTED)
    public void methodB() {
        // TODO 業務代碼
    }
}

當 methodB() 執行失敗後進行回滾,methodA() 也會回滾到 savepoint 點上,而 methodA() 捕獲了 methodB() 拋出的異常,繼續執行自己的業務代碼。

基於註解 @Transactional 聲明事務失效分析

在開發過程中,可能會遇到使用 @Transactional 進行事務管理時出現失效的情況。這裏我們的討論是基於事務的默認傳播行爲是 REQUIRED

常見失效場景

  • 如果使用 MySQL 且引擎是 MyISAM,則事務會不起作用,原因是 MyISAM 不支持事務,改成 InnoDB 引擎則支持事務。

  • 註解 @Trasactional 只能加在 public 修飾的方法上事務才起效。如果加在 protectprivate 等非 public 修飾的方法上,事務將失效。

  • 如果在開啓了事務的方法內,使用了 try-catch 語句塊對異常進行了捕獲,而沒有將異常拋到外層,事務將不起效。

  • 在不同類之間的方法調用中,如果 A 方法開啓了事務,B 方法沒有開啓事務,B 方法調用了 A 方法。如果 B 方法中發生異常,但不是調用的 A 方法產生的,則異常不會使 A 方法的事務回滾,此時事務無效。如果 B 方法中發生異常,異常是調用的 A 方法產生的,則 A 方法的事務回滾,此時事務有效。在 B 方法上加上註解 @Trasactional,這樣 A 和 B 方法就在同一個事務裏了,不管異常產生在哪裏,事務都是有效的。
    簡單地說,不同類之間方法調用時,異常發生在無事務的方法中,但不是被調用的方法產生的,被調用的方法的事務無效。只有異常發生在開啓事務的方法內,事務纔有效。

  • 如果使用了Spring + MVC,則 context:component-scan 重複掃描問題可能會引起事務失效。

原因分析

在應用系統調用聲明 @Transactional 的目標方法時,Spring Framework 默認使用 AOP 代理,在代碼運行時生成一個代理對象,再由這個代理對象來統一管理。

Spring 事務是使用 AOP 環繞通知和異常通知,就是對方法進行攔截,在方法執行前開啓事務,在捕獲到異常時進行事務回滾,在方法執行完成後提交事務。

最後

Spring 團隊建議在具體的類(或類的方法)上使用 @Transactional 註解,而不要使用在類所要實現的任何接口上。在接口上使用 @Transactional 註解,只能當你設置了基於接口的代理時它才生效。因爲註解是不能繼承的,這就意味着如果正在使用基於類的代理時,那麼事務的設置將不能被基於類的代理所識別,而且對象也將不會被事務代理所包裝。

Spring 文檔中寫到:Spring AOP 部分使用 JDK 動態代理或者 CGLIB 來爲目標對象創建代理,如果被代理的目標對象實現了至少一個接口,則會使用 JDK 動態代理。所有該目標類型實現的接口都將被代理。若該目標對象沒有實現任何接口,則創建一個CGLIB代理。

參考

https://juejin.im/post/5b00c52ef265da0b95276091#heading-9

https://blog.csdn.net/rylan11/article/details/76609643

https://blog.csdn.net/justloveyou_/article/details/73733278

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