RestTemplate 引起的 "enq: TX - Row Lock Contention"
一個需求:服務例項 A 和服務例項 B 是同一個應用的不同例項,只是資料庫不同。現在 A 建立一些資料並做分析,程式對所有 ID 特殊處理後原封不動地插入到 B 上並且做同樣的分析,大致程式碼如下:
@Transactional public Integer processRestRequest(CohortSyncDTO dto){ logger.info("進入單佇列遠端REST請求的處理方法 processRestRequest"); CohortDefinition cohorDef = dto.getCohorDef(); cleanupService.deleteCohortData(cohorDef.getId()); .... (此處插入資料程式碼省略) prepareDataAndGenChort(cohorDef, dto.getCohortDetail(), dto.getCohortConceptSetList(), null); logger.info("將分析選擇的 Tables, Statistics, Models 等分析,如果選擇了"); new SpringRest<>().doPost(ConfigUtils.getSysConfig("LOCAL_VINCI") + "/groupStat", dto.getStatInput()); logger.info("end of processRestRequest"); return 1; }
Spring Rest 的程式碼是呼叫本應用的一個Controller 方法:
日誌列印到 2 這行就一直不動了,讓資料庫的夥伴們幫忙找哪條資料庫語句執行出問題了,給出了下面的語句並找到了被掛起的業務 SQL,也就是上圖 3 這行程式碼。
Google了下,找到了 enq: TX - Row Lock Contention Error 的文章,裡面有句:“This occurs when one application is updating or deleting a row that another session is also trying to update or delete. ” 所以,在執行這個 delete 的業務語句之前,肯定有事務裡對同樣的記錄進行過刪除或者更新操作。經檢查,processRestRequest() 裡面的 deleteCohortData() 裡面執行了同一行的資料庫操作。
4 這個語句雖然和 3 不完全一樣,但是都是刪除指定 ID 的行,導致了衝突。回過頭來看 processRestRequest() 方法,其上註解了 @Transactional,也就是說我們希望裡面的所有程式碼都是在一個事務裡完成的,為什麼客觀上造成了在不同事務裡的資料庫行操作。從後臺的日誌,可以看到,在processRestRequest() 執行 Spring Rest 這行後,程式進入了另一個執行緒,從日誌裡面的執行緒號就可以直觀看出來。
2017-11-27 16:04:10 http-nio-8080-exec-15 [com.hebta.vinci.service.collaboration.SyncCohortService]-[INFO] 刪除臨時表GTT_PrimaryCriteriaEvents 2017-11-27 16:04:10 http-nio-8080-exec-15 [com.hebta.vinci.service.collaboration.SyncCohortService]-[INFO] 將分析選擇的 Tables, Statistics, Models 等分析,如果選擇了 2017-11-27 16:04:11 http-nio-8080-exec-14 [com.hebta.vinci.controller.collaboration.BroadcastAdvice]-[INFO] BroadcastAdvice::beforeExecution 執行前攔截請求,獲取引數 2017-11-27 16:04:11 http-nio-8080-exec-14 [com.hebta.vinci.controller.collaboration.BroadcastAdvice]-[INFO] 其他分院開始同步打包的迴歸等統計分析 2017-11-27 16:04:11 http-nio-8080-exec-14 [com.hebta.vinci.controller.collaboration.BroadcastAdvice]-[INFO] 是單佇列的統計分析 2017-11-27 16:04:11 http-nio-8080-exec-14 [com.hebta.vinci.controller.stat.StatisticalController]-[INFO] 進入Group Statistics方法 runGroupStatistics() 2017-11-27 16:04:11 http-nio-8080-exec-14 [com.hebta.vinci.service.stat.BaseStatService]-[INFO] 進入統計分析實體的例項化和儲存方法 createStatisticAnalysis() 2017-11-27 16:04:11 http-nio-8080-exec-14 [com.hebta.vinci.service.stat.BaseStatService]-[INFO] 統計依賴的佇列 ID 和變數 ID
再來看看 SpringRest 這個類的實現,我是對 Spring 的 RestTemplate 進行了簡單的封裝:
這裡可以看到 RestTemplate 新建了一個 HttpRequest 請求,我們知道,一個事務只能在一個執行緒裡,所以 RestTemplate 請求 Controller 的方法是執行在一個新的執行緒裡,也就是脫離了外圍的 Transaction 的控制範圍。所以外圍的事務如果沒有提交,而新執行緒裡有對同一個資源做操作時,就出現了 “enq: TX - Row Lock Contention Error”。
既然間接地呼叫了 RestTemplate 並且它建立了一個新執行緒,為什麼主執行緒沒有立即提交事務?因為我們的對 RestTemplate 封裝的 SpringRest 卻在傻傻地等待這個新執行緒執行結束。不結束就一直 Hold 住主執行緒,子執行緒一直獲得不到資源鎖,就一直掛在那裡。
原因釐清了,解決起來就比較簡單了,那就是在主執行緒直接讓 SpringRest 的程式碼整個地執行在一個執行緒裡,這樣主執行緒可以立即返回,結束方法,提交事務,不影響 RestTemplate 代理的業務邏輯執行。
@Transactional
public Integer processRestRequest(CohortSyncDTO dto){
logger.info("進入單佇列遠端REST請求的處理方法 processRestRequest");
CohortDefinition cohorDef = dto.getCohorDef();
cleanupService.deleteCohortData(cohorDef.getId());
.... (此處插入資料程式碼省略)
prepareDataAndGenChort(cohorDef, dto.getCohortDetail(), dto.getCohortConceptSetList(), null);
logger.info("將分析選擇的 Tables, Statistics, Models 等分析,如果選擇了");
new Thread(() -> {
new SpringRest<>().doPost(ConfigUtils.getSysConfig("LOCAL_VINCI") + "/groupStat", dto.getStatInput());
}) { }.start();
logger.info("end of processRestRequest");
return 1;
}
-----------2017/11/28 更新----------
上面的解決方案有問題,processRestRequest() 裡面的執行緒執行可能早於上面的事務提交完畢,這個時間差是可能存在的,還會出現資料庫行鎖競爭。所以,應當把上面的需要提交的業務邏輯封裝在一個 Spring 事務管理的方法,而 processRestRequest() 則不用也不能使用 Spring 的事務管理了。具體程式碼如下: