RestTemplate 引起的 "enq: TX - Row Lock Contention"

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Allen_jinjie/article/details/78646754

一个需求:服务实例 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 的事务管理了。具体代码如下:


猜你喜欢

转载自blog.csdn.net/Allen_jinjie/article/details/78646754