1. 程式人生 > >RestTemplate 引起的 "enq: TX - Row Lock Contention"

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 的事務管理了。具體程式碼如下: