使用TransactionTemplate不當,導致事務異常的問題
最近上線有個異常,常見的MYSQL等待鎖超時異常。出現的情況很奇怪:
- 有些機器異常,有些機器不可以。
- 重啟后,異常消失
- 運行一段時間后,都出現異常
根據數據庫連接的日志,發現TransactionTemplate
使用了一個新數據庫連接,和外層方法中使用的不是同一個數據庫連接。
外層方法有注解事務,會更新A記錄,TransactionTemplate
中的模板方法也會更新A記錄
TransactionTemplate
默認的傳播特性是PROPAGATION_REQUIRED
,所以A方法上有注解事務,在同一線程調用棧中,肯定使用的是同一個連接和同一個事務。
按照理論來說,不應該在模板方法中會存在鎖等待。
線上偶然發升,時間久了必然發生,本地簡單測試無法復現。
經過查看TransactionTemplate
源碼,發現重新獲取連接有兩種情況: - 當前線程沒有綁定事務
- 當前線程綁定了事務,但是傳播行為
PROPAGATION_REQUIRES_NEW
在綁定事務的方法中,使用的ThreadLocal來操作的,而且Spring的事務這塊理論不可能出問題。
經過2天的源碼分析,猜測出問題只能是傳播行為變了。
TransactionTemplate
繼承于DefaultTransactionDefinition
,默認的傳播特性是PROPAGATION_REQUIRED
要改變其值,需要調用setPropagationBehavior
TransactionTemplate
默認由spring容器初始化,并且是單例模式。
在整個項目中搜索,setPropagationBehavior
方法的代碼,果不其然,有如下一段代碼:
@Autowired
private TransactionTemplate template;
private void requireNewTransaction(Consumer<TransactionStatus> action) {
int originalPropagationBehavior = template.getPropagationBehavior();
int originalIsolationLevel = template.getIsolationLevel();
try {
template.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
template.executeWithoutResult(action);
} finally {
template.setIsolationLevel(originalIsolationLevel);
template.setPropagationBehavior(originalPropagationBehavior);
}
}
大致用途 就是想要在編程事務的時候,新開事務去保存,先落庫。
砸一下看,好像沒問題。
仔細分析一下,發現template是單例模式,多線程情況下去修改單例模式的屬性,然后在還原,又沒有加鎖,必然會出現競爭導致后面的線程獲取到不正確的值。
這就是解釋了為啥有些機器不會出問題,可能是有些機器,線程并發時還沒有導致傳播行為錯誤
也解釋了為啥在運行長一段時間后,所有機器都會出問題。
注解事務和編程事務可以一起使用沒有問題,他們底層都會使用Spring的事務同步器進行同步。
如果要在模板事務中新開事務進行操作,可以不用單例模式的TransactionTemplate
。如下操作依然可以實現:
private void requireNewTransaction2(Consumer<TransactionStatus> action) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
template.executeWithoutResult(action);
}
總結
通過上面的場景分析,如果一個異常有如下情況,可以嘗試考慮多線程共享數據一致性問題:
- 不是所有的實例都會出現錯誤
- 出現錯誤的問題偶發,并且某一臺實例出現錯誤后,會一直錯誤
- 長時間運行后,所有實例都會出現錯誤