事務傳播機制之REQUIRES_NEW

yuqiu2004發表於2024-11-10

起因是完成本學期JavaEE課程作業之JdbcTemplate使用與Transactional中幾種事務傳播機制,其中有一個場景就是主事務中呼叫另一個事務,然後在主事務中引發異常,要求不影響到呼叫的事務。顯然,應該使用REQUIRES_NEW方式傳播(即被呼叫的事務方法使用@Transactional(propagation = Propagation.REQUIRES_NEW)來開啟一個新事務

樣例程式碼如下:

	...
	@Override
    @Transactional
    public void RollBackAfterLog(String fromAccount, String toAccount, BigDecimal amount) throws Exception {
        transfer(fromAccount, toAccount, amount);
//        System.out.println("outside obj: " + beanFactory.getBean(BankService.class).getClass());
        Integer transactionId = logTransaction(fromAccount, toAccount, amount);
        if (amount.intValue() > 5) throw new RuntimeException("轉賬金額過大");
        auditTransaction(transactionId, "FENDING");
    }

	@Transactional(propagation = Propagation.REQUIRES_NEW) // 建立新的事務 不受影響
    @Override
    public Integer logTransaction(String fromAccount, String toAccount, BigDecimal amount) {
        return transactionLogDao.insert(fromAccount, toAccount, amount);
    }
	...

但是遺憾的是,以上程式碼會導致主事務和被呼叫事務都會滾!

(此段為廢話)我一臉矇蔽,這不是騙人嗎,說好的開啟新事務呢!接著我逐步除錯,發現資料庫確實從未新增過資料,蛤?其實這裡我除錯時會先進入代理類的方法體,我就該有所聯想...但是受於智商所限,我並沒有發現問題所在,遂詢問chat-gpt,gpt亂說一通,不曾切入要害,最終只能搜尋部落格求解,還好普天之下高手雲集+我的關鍵詞搜尋能力出群(bushi)終於找到的解決的方法。

首先,@Transactional的實現原理,其實就是AOP代理,在這個代理方法內自動的加上事務的開啟以及事務的提交或者回滾。所以問題也就出在這裡,在主事務方法的地方,確實是代理類來呼叫的,但是,主事務呼叫別的事務方法卻是呼叫的原生的方法,類似於如下情況:

// 代理物件 BankServiceImplProxy
public class BankServiceImplProxy extends BankServiceImpl {
    @Override
    public void RollBackAfterLog(...) {
        // 事務管理程式碼
        super.RollBackAfterLog(...);
        // 提交或回滾事務
    }

    @Override
    public void logTransaction(...) {
        // 事務管理程式碼
        super.logTransaction(...);
        // 提交或回滾事務
    }
}

所以這裡的logTransaction()的事務被繞過了,從而導致了上述的錯誤。

最後,解決方法就是:手動呼叫代理類的代理方法而非原生的非事務方法。具體方式有兩種,一個是透過AopContext獲取對應的代理類,另一種就是透過BeanFactory獲取代理類(getBean() or DI)。我這裡採取的第二種,但是需要注意迴圈依賴的問題。

	private BankService bankServiceProxy;

    @Resource
    public void setBankServiceProxy(@Lazy BankService bankService) { // 延遲初始化 避免迴圈依賴
        this.bankServiceProxy = bankService;
    }


	@Override
    @Transactional
    public void RollBackAfterLog(String fromAccount, String toAccount, BigDecimal amount) throws Exception {
        bankServiceProxy.transfer(fromAccount, toAccount, amount);
//        System.out.println("outside obj: " + beanFactory.getBean(BankService.class).getClass());
        Integer transactionId = bankServiceProxy.logTransaction(fromAccount, toAccount, amount);
        if (amount.intValue() > 5) throw new RuntimeException("轉賬金額過大");
        bankServiceProxy.auditTransaction(transactionId, "FENDING");
    }

參考文章:

解決Spring子事務新開事務REQUIRES_NEW仍被主事務回滾問題

spring事務傳播行為之使用REQUIRES_NEW不回滾

spring事務傳播機制

相關文章