1、背景
有一个报告功能,报告需要生成1个word,6个excel附件,总共7个文件,需要记录报告生成进度,进度字段jd初始化是0,每个文件生成成功进度加1,生成失败就把生成状态置为失败。
更新进度语句:update bg set jd = jd+1 where id = 'xx'
上线一段时间后,很多报告进度都没有100%
2、问题排查
查看线上日志,发现生成附件2、附件3有时候会报错,然后对着报错改了代码,还是觉得有问题。因为看了代码,7个文件生成用的7个线程,每个结构都是try,catch,finnally,
看下面代码,感觉每个子线程都是走到finally里面,那就要么更新进度为100%,要么更新状态为失败。
public void creatBgExcel(CreateBgWjPo createBgWjPo) {
..............................
//附件1
CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
//新增数据excel,附件1
createXzsjExcel(createBgWjPo, tempFileDir);
}, threadPoolExecutor);
//附件2
CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> {
//缺失数据excel,附件2
createWtsjExcel(createBgWjPo, tempFileDir);
}, threadPoolExecutor);
...............................
CompletableFuture<Void> headerFuture = CompletableFuture.allOf(task1,task2 ,task3,.......);
headerFuture.join();
log.info("--bgId:{},excel报告单个附件已经全部生成", bgId);
}
public void createWtsjExcel(CreateBgWjPo createBgWjPo, String tempFilePath) { log.info("--附件2,问题数据excel,开始生成数据,bgmc:{}", createBgWjPo.getGxZlbg().getBgmc()); String exelxx = ""; boolean isSucess = false; String msg = ""; try{ dosomething(); isSucess = true; }catch (Exception e) { log.error("createWtsjExcel附件2生成失败,bgmc:{}", createBgWjPo.getGxZlbg().getBgmc(),e); msg = "附件2生成excel失败.失败原因:{}" + e.getMessage(); } finally { if (isSucess) { //更新进度 gxZlbgMapper.updateBgJd(createBgWjPo.getGxZlbg().getId()); } else { //更新状态 bgZtToFail(createBgWjPo.getGxZlbg().getId(), msg, false, true); } } } (-)怀疑1,难道没有进入finally方法? finanlly一般不执行的情况: 1、代码存在死循环 try{ while(true){ }catch (Exception e) { } finally { } 排查了代码,没有死循环,显然不适用。按理只要进入了子方法,肯定会进入finally。 排查线上日志docker logs -f --tail 100000 api-xx grep 有问题的报告名称 发现: 有问题的报告名称,有些子文件没有打印出 开始生成数据的日志 结论: 有些报告生成的时候,根本没有进入对应的子方法 (2)排查方法外层代码 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(8, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(Integer.MAX_VALUE)); @Override public SwaggerResultUtil<String> createZlbg(String bgId,boolean isReCreate) { CompletableFuture.runAsync(() -> { GxZlbg zlbg = new GxZlbg(); zlbg.setId(bgId); //更新附件地址 zlbg.setFjsczt(BgztEnum.DOING.getCode()); zlbg.setWdsczt(BgztEnum.DOING.getCode()); gxZlbgMapper.updateById(zlbg); CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> { //创建excel creatBgExcel(result.getData()); }, threadPoolExecutor); CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> { //创建word createBgWord(result.getData()); }, threadPoolExecutor); CompletableFuture<Void> headerFuture = CompletableFuture.allOf(task1, task2); headerFuture.join(); }, threadPoolExecutor); return SwaggerResultUtil.resultSuccess(); }
看到这段代码,发现存在线程池套线程池。
线程池8个,核心线程10个,问题分析
任务 | 外部线程 | 执行任务内部线程 | 备注 |
---|---|---|---|
任务1 | 3 | 6 | 里面子任务被执完成,外层的线程才会被释放 |
任务2 | 3 | 6 | |
任务3 | 3 | 6 |
这样如果有3个报告同时生成,而且子文件方法耗时长,就会出现线程都被外部线程占用,内部无线程可用的状况,出现死锁,后续报告都无法开始生成。跟线上问题完全符合,应该就是发生了线程死锁
3、问题解决
(1)不使用线程池套线程池的办法,把最外层方法指定线程池去掉threadPoolExecutor。
存在问题,如果里面线程池最大线程数10,执行里层所有方法需要线程数据>10, 外部没有线程池,最大可能10个任务同时并发,10个任务互相等待子线程,虽然没有死锁,但是每个任务都很慢,而且可能引起爆内存。这个方案只适用很少的场景
(2)外层一个线程池,里面一个线程池。 这样可以控制外层是按顺序执行,以及控制外层的并发数
(3)所有任务,先写入redis队列,然后定时任务巡检redis队列数据,取出任务一个个执行。 redis+定时任务+线程池方案, 这个方案,很容易排查哪里容易没有执行
上面方案按具体需求选取