起因是完成本學期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事務傳播機制