记录使用easypoi和线程池不当导致的bug
1、背景
在codeing需求时,接到了相关人员的反馈,为什么我导出失败了,经过排查发现随着数据量的不断增大,对于导出场景出现了巨大的瓶颈,为了解决出现的这个问题,经历了几个版本的变动
- 后台页面直接导出(简单粗暴,缺点是后台响应时间长,随着数据量的提升可能需要不断的调整超时时间)
- 邮件发送
- 文件下载中心 (将所有的文件导出操作 都集中保存到一张文件下载记录表,不是所有业务都适用)
2、邮件发送模式
在首次解决导出超时问题时,经过链路检查我发现,耗时主要集中在查询和组装文件,但这两步无法避免,这时我们考虑到对于相关人员来说导出不止限于页面,从结果来看相关人员只是想要获取自己需要的文件,并不关注文件是通过什么渠道的,于是第一版 邮件版本的调整出现了,在这时相关的业务操作一般都会观察自己的邮箱是否收到邮件才会跳转到下一个页面继续导出
下面是步骤流程
@RequestMapping("/export.json")
public boolean export() {
CompletableFuture.runAsync(() -> {
// 上锁 防止同一个文件被相关人员多次点击导出
// 查询需要的数据
// 根据数据组装文件
// 邮件发送
});
return true;
}
在调整后,让相关人员才灰度环境验证发现,当前情况没有问题
3、文件下载中心模式
好景不长,又过了几个月,相关人员又反馈,我导出申请没有收到邮件,balabala,确认了操作流程没有问题,开始了第二阶段的调整,经过检查发现我司内部 邮件发送接口是走dubbo统一封装,部分场景是存在报文超长,此时邮件也无法执行了,我们需要把整个导出拆分开处理
- 查询数据库获取数据
- 根据查询的数据组装相关的excel文件
- 将文件发送给用户
从步骤看,前两个步骤完全可以和第三个步骤拆分开
简易表结构
CREATE TABLE `file_download_task`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`oss_key` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '文件oss地址key 加密串',
`file_name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '下载文件名称',
`file_suffix` varchar(32) COLLATE utf8mb4_bin NOT NULL COMMENT '文件 后缀',
`user_id` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT '申请人id 加密存储',
`download_time` datetime DEFAULT NULL COMMENT '下载日期',
`params` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '申请相关参数',
`add_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`remarks` varchar(255) COLLATE utf8mb4_bin DEFAULT '' COMMENT '下载文件备注',
`file_type` int(8) NOT NULL DEFAULT '0' COMMENT '导出文件的类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='文件下载列表';
我们将 查询数据库获取数据 和 组装表格文件 放在异步线程,当文件生成成功直接将数据 保存到 file_download_task 中,然后给用户展示一个下载页面,这样的话就不会出现超时了,直接从oss读取文件肯定比组装文件快多了, 调整后灰度验证,没有问题,于是按照这个方案我讲相关的接口做了调整
4、发现线程安全问题
过了一个月,相关人员联系,导出又又又失败了,但是失败场景比较特殊,因为相关的文件都统一走文件下载中心了,操作人员也很喜欢这个方案,相关文件都导出一下,需要哪个随时下载,于是出现了短时间内大量下载的情况,问题产生了,同一个接口在单独调用时成功,但是在并发调用时概率失败,因为测试环境的cpu2c无法复现,只能在预发尝试验证
4.1、CompletableFuture.runAsync
首先检查相关源码,异步执行使用 ForkJoinPool 看似没有问题
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return asyncRunStage(asyncPool, runnable);
}
private static final Executor asyncPool = useCommonPool ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
这是尝试检查 excel导出组件
public static Workbook exportBigExcel(ExportParams entity, Class<?> pojoClass,
Collection<?> dataSet) {
ExcelBatchExportService batchService = ExcelBatchExportService
.getExcelBatchExportService(entity, pojoClass);
return batchService.appendData(dataSet);
}
// 等等 这是个啥threadlocal??? wc
public static ExcelBatchExportService getExcelBatchExportService(ExportParams entity,
Class<?> pojoClass) {
if (THREAD_LOCAL.get() == null) {
ExcelBatchExportService batchServer = new ExcelBatchExportService();
batchServer.init(entity, pojoClass);
THREAD_LOCAL.set(batchServer);
}
return THREAD_LOCAL.get();
}
虽然虽然 ForkJoinPool.commonPool() 不会导致线程安全问题,但在使用 CompletableFuture.runAsync 时,仍然需要注意以下几点:
避免共享资源
每个任务使用独立资源:确保每个异步任务都使用独立的资源(如独立的 Workbook 对象、独立的文件句柄等),避免多个任务共享同一个资源。
4494






