一、基本概念
事務的隔離級別,事務傳播行為見《事務之二: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 了。