事務之六:spring 嵌套事務


一、基本概念

 事務的隔離級別,事務傳播行為見《事務之二:spring事務(事務管理方式,事務5隔離級別,7個事務傳播行為,spring事務回滾條件) 》

二、 嵌套事務示例

2.1、Propagation.REQUIRED+Propagation.REQUIRES_NEW

package dxz.demo1;


@Service
public class ServiceAImpl implements ServiceA {

    @Autowired
    private ServiceB serviceB;
    
    @Autowired
    private VcSettleMainMapper vcSettleMainMapper;
    
    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void methodA() {
        String id = IdGenerator.generatePayId("A");
        VcSettleMain vc = buildModel(id);
        vcSettleMainMapper.insertVcSettleMain(vc);
        System.out.println("ServiceAImpl VcSettleMain111:" + vc);
        
        serviceB.methodB();  
        
        VcSettleMain vc2 = buildModel(id);
        vcSettleMainMapper.insertVcSettleMain(vc2);
        System.out.println("ServiceAImpl VcSettleMain22222:" + vc2);
    }

    private VcSettleMain buildModel(String id) {
        VcSettleMain vc = new VcSettleMain();
        vc.setBatchNo(id);
        vc.setCreateBy("dxz");
        vc.setCreateTime(LocalDateTime.now());
        vc.setTotalCount(11L);
        vc.setTotalMoney(BigDecimal.ZERO);
        vc.setState("5");
        return vc;
    }

}

ServiceB

package dxz.demo1;


@Service
public class ServiceBImpl implements ServiceB {

    @Autowired
    private VcSettleMainMapper vcSettleMainMapper;
    
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false)
    public void methodB() {
        String id = IdGenerator.generatePayId("B");
        VcSettleMain vc = buildModel(id);
        vcSettleMainMapper.insertVcSettleMain(vc);
        System.out.println("---ServiceBImpl VcSettleMain:" + vc);
    }
}

controller

package dxz.demo1;

@RestController
@RequestMapping("/dxzdemo1")
@Api(value = "Demo1", description="Demo1")
public class Demo1 {
    @Autowired
    private ServiceA serviceA;

    /**
     * 嵌套事務測試
     */
    @PostMapping(value = "/test1")
    public String methodA() throws Exception {
        serviceA.methodA();
        return "ok";
    }
}

結果:

看數據庫表記錄:

 

這種情況下, 因為 ServiceB#methodB 的事務屬性為 PROPAGATION_REQUIRES_NEW,ServiceB是一個獨立的事務,與外層事務沒有任何關系。如果ServiceB執行失敗(上面示例中讓ServiceB的id為已經存在的值),ServiceA的調用出會拋出異常,導致ServiceA的事務回滾。

並且, 在 ServiceB#methodB 執行時 ServiceA#methodA 的事務已經掛起了 (關於事務掛起的內容已經超出了本文的討論范圍)。

2.2、Propagation.REQUIRED+Propagation.REQUIRED

//ServiceA
//...
    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void methodA() {

//ServiceB
//...
    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void methodB(String id) {
//...

--“1”可插入,“2”可插入,“3”不可插入:

結果是“1”,“2”,“3”都不能插入,“1”,“2”被回滾。

--“1”可插入,“2”不可插入,“3”可插入:

結果是“1”,“2”,“3”都不能插入,“1”,“2”被回滾。

2.3、Propagation.REQUIRED+無事務注解

//ServiceA
//...
    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void methodA() {

//ServiceB
//...
    @Override
    //沒有加事務注解
    public void methodB(String id) {
//...

--“1”可插入,“2”可插入,“3”不可插入:

結果是“1”,“2”,“3”都不能插入,“1”,“2”被回滾。

2.4、內層事務被try-catch:

2.4.1、trycatch+Propagation.REQUIRED+Propagation.REQUIRED

//ServiceA
//...
    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void methodA() {
        try {
            serviceB.methodB(id);
        } catch (Exception e) {
            System.out.println("內層事務出錯啦。");
        }
    }

//ServiceB
//...
    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void methodB(String id) {
//...

--“1”可插入,“2”不可插入,“3”可插入:

結果是“1”,“2”,“3”都不能插入,“1”被回滾。

事務設置為Propagation.REQUIRED時,如果內層方法拋出Exception,外層方法中捕獲Exception但是並沒有繼續向外拋出,最后出現“Transaction rolled back because it has been marked as rollback-only”的錯誤。外層的方法也將會回滾。

其原因是:內層方法拋異常返回時,transacation被設置為rollback-only了,但是外層方法將異常消化掉,沒有繼續向外拋,那么外層方法正常結束時,transaction會執行commit操作,但是transaction已經被設置為rollback-only了。所以,出現“Transaction rolled back because it has been marked as rollback-only”錯誤。

2.4.2、trycatch+Propagation.REQUIRED+Propagation.NESTED

//ServiceA
//...
    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void methodA() {
        try {
            serviceB.methodB(id);
        } catch (Exception e) {
            System.out.println("內層事務出錯啦。");
        }
    }

//ServiceB
//...
    @Override
    @Transactional(propagation = Propagation.NESTED, readOnly = false)
    public void methodB(String id) {
//...

--“1”可插入,“2”不可插入,“3”可插入:

結果是“1”,“3"記錄插入成功,“2”記錄插入失敗。

說明:

當內層配置成 PROPAGATION_NESTED, 此時兩者之間又將如何協作呢? 從 Juergen Hoeller 的原話中我們可以找到答案, ServiceB#methodB 如果 rollback, 那么內部事務(即 ServiceB#methodB) 將回滾到它執行前的 SavePoint(注意, 這是本文中第一次提到它, 潛套事務中最核心的概念), 而外部事務(即 ServiceA#methodA) 可以有以下兩種處理方式:
1、內層失敗,外層調用其它分支,代碼如下

ServiceA {  
      
    /** 
     * 事務屬性配置為 PROPAGATION_REQUIRED 
     */  
    void methodA() {  
        try {  
            ServiceB.methodB();  
        } catch (SomeException) {  
            // 執行其他業務, 如 ServiceC.methodC();  
        }  
    }  
  
}  

這種方式也是潛套事務最有價值的地方, 它起到了分支執行的效果, 如果 ServiceB.methodB 失敗, 那么執行 ServiceC.methodC(), 而 ServiceB.methodB 已經回滾到它執行之前的 SavePoint, 所以不會產生臟數據(相當於此方法從未執行過), 這種特性可以用在某些特殊的業務中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都沒有辦法做到這一點。
2. 代碼不做任何修改, 那么如果內部事務(即 ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滾到它執行之前的 SavePoint(在任何情況下都會如此), 外部事務(即 ServiceA#methodA) 將根據具體的配置決定自己是 commit 還是 rollback。

三、嵌套事務總結

使用嵌套事務的場景有兩點需求:

  • 需要事務BC與事務AD一起commit,即:作為事務AD的子事務,事務BC只有在事務AD成功commit時(階段3成功)才commit。這個需求簡單稱之為“聯合成功”。這一點PROPAGATION_NESTED和PROPAGATION_REQUIRED可以做到。
  • 需要事務BC的rollback不(無條件的)影響事務AD的commit。這個需求簡單稱之為“隔離失敗”。這一點PROPAGATION_NESTED和PROPAGATION_REQUIRES_NEW可以做到。

分解下,可知PROPAGATION_NESTED的特殊性有:

1、使用PROPAGATION_REQUIRED滿足需求1,但子事務BC的rollback會無條件地使父事務AD也rollback,不能滿足需求2。即使對子事務進行了try-catch,父事務AD也不能commit。示例見2.4.1、trycatch+Propagation.REQUIRED+Propagation.REQUIRED

2、使用PROPAGATION_REQUIRES_NEW滿足需求2,但子事務(這時不應該稱之為子事務)BC是完全新的事務上下文,父事務(這時也不應該稱之為父事務)AD的成功與否完全不影響BC的提交,不能滿足需求1。

同時滿足上述兩條需求就要用到PROPAGATION_NESTED了。PROPAGATION_NESTED在事務AD執行到B點時,設置了savePoint(關鍵)。

當BC事務成功commit時,PROPAGATION_NESTED的行為與PROPAGATION_REQUIRED一樣。只有當事務AD在D點成功commit時,事務BC才真正commit,如果階段3執行異常,導致事務AD rollback,事務BC也將一起rollback ,從而滿足了“聯合成功”。

 當階段2執行異常,導致BC事務rollback時,因為設置了savePoint,AD事務可以選擇與BC一起rollback或繼續階段3的執行並保留階段1的執行結果,從而滿足了“隔離失敗”。

當然,要明確一點,事務傳播策略的定義是在聲明或事務管理范圍內的(首先是在EJB CMT規范中定義,Spring事務框架補充了PROPAGATION_NESTED),編程式的事務管理不存在事務傳播的問題。

四、PROPAGATION_NESTED的必要條件

上面大致講述了潛套事務的使用場景, 下面我們來看如何在 spring 中使用 PROPAGATION_NESTED, 首先來看 AbstractPlatformTransactionManager 

/** 
 * Create a TransactionStatus for an existing transaction. 
 */  
private TransactionStatus handleExistingTransaction(  
        TransactionDefinition definition, Object transaction, boolean debugEnabled)  
        throws TransactionException {  
  
   ... 省略  
  
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {  
        if (!isNestedTransactionAllowed()) {  
            throw new NestedTransactionNotSupportedException(  
                    "Transaction manager does not allow nested transactions by default - " +  
                    "specify 'nestedTransactionAllowed' property with value 'true'");  
        }  
        if (debugEnabled) {  
            logger.debug("Creating nested transaction with name [" + definition.getName() + "]");  
        }  
        if (useSavepointForNestedTransaction()) {  
            // Create savepoint within existing Spring-managed transaction,  
            // through the SavepointManager API implemented by TransactionStatus.  
            // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.  
            DefaultTransactionStatus status =  
                    newTransactionStatus(definition, transaction, false, false, debugEnabled, null);  
            status.createAndHoldSavepoint();  
            return status;  
        }  
        else {  
            // Nested transaction through nested begin and commit/rollback calls.  
            // Usually only for JTA: Spring synchronization might get activated here  
            // in case of a pre-existing JTA transaction.  
            doBegin(transaction, definition);  
            boolean newSynchronization = (this.transactionSynchronization != SYNCHRONIZATION_NEVER);  
            return newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null);  
        }  
    }  
} 


1. 我們要設置 transactionManager 的 nestedTransactionAllowed 屬性為 true, 注意, 此屬性默認為 false!!! 
再看 AbstractTransactionStatus#createAndHoldSavepoint() 方法 

/** 
 * Create a savepoint and hold it for the transaction. 
 * @throws org.springframework.transaction.NestedTransactionNotSupportedException 
 * if the underlying transaction does not support savepoints 
 */  
public void createAndHoldSavepoint() throws TransactionException {  
    setSavepoint(getSavepointManager().createSavepoint());  
}  

  可以看到 Savepoint 是 SavepointManager.createSavepoint 實現的, 再看 SavepointManager 的層次結構, 發現 
  其 Template 實現是 JdbcTransactionObjectSupport, 常用的 DatasourceTransactionManager, HibernateTransactionManager 
  中的 TransactonObject 都是它的子類 : 
  JdbcTransactionObjectSupport 告訴我們必須要滿足兩個條件才能 createSavepoint : 
 2. java.sql.Savepoint 必須存在, 即 jdk 版本要 1.4+ 
 3. Connection.getMetaData().supportsSavepoints() 必須為 true, 即 jdbc drive 必須支持 JDBC 3.0 
確保以上條件都滿足后, 你就可以嘗試使用 PROPAGATION_NESTED 了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM