ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

xllg 结算单导出 10 万行,Excel 写崩了:EasyExcel 三个优化,内存降到 1/10

xllg 结算单导出 10 万行,Excel 写崩了:EasyExcel 三个优化,内存降到 1/10 导读零工系统要给企业批量导出结算单一次 10 万行。第一次我图省事查出 List 直接 EasyExcel 写结果堆内存 512MB 直接 OOM接口 30 秒超时。后来分页流式写 自定义样式 异步导出三招下来内存占用降了 90%这篇把优化过程完整记下来。xllg 结算单导出 10 万行Excel 写崩了EasyExcel 三个优化内存降到 1/10先说事故现场。企业财务要导当月零工结算单10 万条记录带 20 个字段。第一版代码很朴素查全量 ListEasyExcel.write(outputStream).sheet().doWrite(list)一把梭。结果接口直接 OOM服务重启。看 GC 日志Full GC 连续触发堆 512MB 顶不住。第一个优化分页查询 流式写入EasyExcel 本身是流式写的SAX 解析但doWrite(list)会把整个 List 一次性喂进去。问题不在 EasyExcel在我把 10 万行数据全查进了内存。改成查一批写一批Transactional(readOnlytrue)publicvoidexportSettleDetail(OutputStreamout,ExportQueryquery){ExcelWriterwriterEasyExcel.write(out,SettleDetailVO.class).build();WriteSheetsheetEasyExcel.writerSheet(结算明细).build();intpageSize5000;longpage1;while(true){// 分页查询查一批写一批ListpageDatasettleMapper.selectPageData(query,page,pageSize);if(pageData.isEmpty()){break;}writer.write(pageData,sheet);pageData.clear();// 释放引用让 GC 能回收page;}writer.finish();}关键就两点分页查limit offset 写完一批立即 clear。内存里最多同时存在 5000 行的 VO10 万行也不再是问题。第二个优化样式别一行一设第一版给单元格加了边框和列宽ExcelWriteHandler里每个 Cell 都cellStyle.setBorderBottom10 万行 × 20 列 200 万次样式设置光样式对象就吃掉一大块内存。EasyExcel 的优化是预创建样式复用同一个 CellStylepublicclassSettleStyleHandlerimplementsWriteHandler{privateCellStyleheadStyle;privateCellStylecontentStyle;Overridepublicvoidsheet(intsheetNo,Sheetsheet){// 只创建一次样式全局复用headStylecreateHeadStyle(sheet.getWorkbook());contentStylecreateContentStyle(sheet.getWorkbook());}Overridepublicvoidcell(intcellIndex,Cellcell,WriteSheetHoldersheetHolder,WriteWorkbookHolderworkbookHolder){if(cellIndex0){cell.setCellStyle(headStyle);// 表头用头样式}else{cell.setCellStyle(contentStyle);// 数据行统一复用}}}sheet()回调里创建样式、cell()里只 setCellStyle 引用。样式对象从 200 万个降到 2 个内存省了一大截。第三个优化异步导出别让 HTTP 请求干等10 万行哪怕优化了也要写 3-5 秒同步接口在浏览器里就是转圈到超时。改成异步接口先返回任务 ID后台线程导出导出完存 OSS前端轮询任务状态再下载ServicepublicclassExportService{privatefinalMaptaskMapnewConcurrentHashMap();Async(exportExecutor)publicvoidasyncExport(LongtaskId,ExportQueryquery){ExportTasktasktaskMap.computeIfAbsent(taskId,k-newExportTask());task.setStatus(ExportStatus.RUNNING);try{StringossKeydoExport(query);// 分页流式写入 OSS 临时文件task.setFileUrl(ossKey);task.setStatus(ExportStatus.SUCCESS);}catch(Exceptione){log.error(导出失败 taskId{},taskId,e);task.setStatus(ExportStatus.FAILED);}}privateStringdoExport(ExportQueryquery)throwsIOException{// 用 EasyExcel 写本地临时文件或直接写 OSS 的流分页查询逻辑同上returnuploadToOss(tempFile);}}RestControllerpublicclassExportController{PostMapping(/settle/export)publicResultsubmitExport(RequestBodyExportQueryquery){longtaskIdSystem.currentTimeMillis()%100000ThreadLocalRandom.current().nextInt(1000);exportService.asyncExport(taskId,query);returnResult.success(taskId);// 前端拿着 taskId 轮询}GetMapping(/settle/export/status)publicResultqueryStatus(RequestParamLongtaskId){returnResult.success(exportService.getStatus(taskId));}}异步导出 任务状态轮询用户点导出后该干嘛干嘛导出完成收到链接再下载。踩坑记录临时文件撑爆磁盘现象导出功能上线第三天服务器磁盘告警/tmp 目录被临时文件塞满。排查过程看目录发现一堆easyExcel_xxx.tmp文件每个几百 MB。查代码异常分支里writer.finish()没执行临时文件没人清理finally里也没删。定位思路EasyExcel 写文件时内部会建临时文件finish()或者流关闭时才清理。我的doExport方法异常时直接抛出去清理逻辑没跟上。最终解决doExport改成 try-finallyfinally 里删临时文件同时加了一个定时任务每天凌晨清理超过 24 小时的导出临时文件。异步任务最容易漏的是资源清理异常分支必须走 finally。可直接复用的要点分页查询 流式写入是 EasyExcel 大导出的核心内存里最多保留一批数据。样式在sheet()回调里预创建cell()里只复用引用别每行新建 CellStyle。大导出必须异步化任务 ID 状态轮询 文件链接别让 HTTP 干等。导出文件的清理写进 finally异常分支也要删临时文件。导出 VO 字段尽量精简20 个字段已经是上限别一把梭 50 个列。项目源码https://gitee.com/gzqkl/xllg
返回列表