1. 问题
线上一个服务,放量之后出现比较多的 long sql。
经过分析,问题出在以下逻辑:
- 主线程每次拉取一批任务并检查任务是否已经存在
- 如果存在则准备执行;否则将任务插入 db。然后准备执行这批任务
- 每个任务使用线程池的一个线程去执行,在子线程中检查任务是否符合条件,符合就将任务状态更改为“进行中”并执行,否则直接结束,并在任务完成后更新为“已完成”
- 主线程等待1s,如果期间所有子任务都完成了,返回成功,否则超时结束,待下次启动再检查这些任务是否已经完成,没完成的下次继续。
慢有如下特点:
- long sql 都是更新状态为“进行中”的sql,更新状态为“已完成”的完全没有 long sql
- long sql 的执行时间几乎都是1s
- 更新状态为“进行中”的 sql,也只有极少部分实际表现是 long sql,大部分都是正常执行的
2. 原因
数据库使用的是“读已提交”的隔离级别。(如果是可重复读也会出现这个问题)
主线程方法上加了 @Transactional 注解, 所以主线程的执行是在一个事务中, 在创建任务插入 db 时会获取对应数据行的写锁;如果是之前已经创建的任务,则不会加锁。
子线程方法也加了 @Transactional 注解。因为更新为“已完成”与更新为“进行中”都是在子线程的事务中,先更新为“进行中”如果已经获得了锁,则更新为“已完成”不会再被阻塞
主线程等待 1s 后结束释放锁,所以 long sql 都是 1s。
每次都是新创建任务占极少部分,大部分都是执行之前已经创建的任务,而已经创建的任务再次执行不会被主线程阻塞,所以 long sql 比例极小。
3. 根本问题所在
db 操作放在了多线程中,而且都用 @Transactional 注解加了事务,而各个线程的事务是不共享的。
4. 解决问题的方法
- 将sql相关的操作抽出到子线程外,即所有db操作都在主线程内执行,缺点是代码变复杂
- 改造 @Transactional 注解,让事务能够传递到线程池中。解决@Transactional不能跨线程池共享事务的问题—使用TransmittableThreadLocal