资讯动态

SXSSFWorkbook+游标分页解决百万级Excel导出OOM

发布时间:2026/10/26 14:46:46 来源:尧图企业网站定制
前阵子帮一个团队看导出接口现象很典型单次导出三五万条数据没事运营一勾“全部”就崩日志里躺着java.lang.OutOfMemoryError: Java heap space堆转储打开一看全是XSSFCell、XSSFRow对象在那儿趴着。这类问题的根子不在数据库也不在网络而在于把整份 Excel 当成一个内存对象在养。后来换成SXSSFWorkbook配合分页查询边查边写同一台 2C4G 的机器上百万行导出稳定在 300MB 堆内存以内跑完。这篇就把 SXSSFWorkbook 实现分页查询导出这件事从头拆一遍它到底解决了什么问题、窗口参数怎么定、分页游标怎么写、样式和临时文件有哪些坑、线上出问题怎么查。适合正在写导出功能的后端同学也适合被 OOM 追着跑、想把方案一次性做扎实的人。1. 为什么大数据量导出必须换掉 XSSFWorkbook1.1 从一次内存溢出说起XSSFWorkbook 的致命短板先把结论摆在前面XSSFWorkbook 是全内存模型它把整个 .xlsx 文件在 JVM 堆里建成一棵完整的对象树Workbook → Sheet → Row → Cell一层套一层每一层都是实打实的 Java 对象。一个 Cell 对象本身加上它持有的字符串、样式引用、共享字符串表索引开销远不止“几个字节”。行业内比较常见的经验值是每行十几个单元格的情况下XSSF 的内存占用大致在每行 1KB 到 3KB 之间具体取决于列数、字符串长度和样式数量。按这个量级估算一下就知道为什么三五万行是分水岭。假设每行 1.5KB10 万行就是 150MB 常驻对象加上写入时 POI 内部要构建的 XML 缓冲、共享字符串表SharedStringsTable 会把所有不重复的字符串都收进去重复率低的业务数据这个表能撑得很大、以及 zip 打包过程的临时结构峰值翻个两三倍很正常。一台默认堆 512MB 或 1GB 的应用跑到几十万行必然撑不住。更难受的是这个 OOM 不是缓慢劣化而是到了某个临界点突然炸压测时不容易复现线上却天天报。还有一层隐形成本全内存模型意味着导出耗时和内存峰值都集中在最后那个write()调用上。前面的数据填充阶段看起来很快最后落盘那一下会卡住几秒甚至几十秒接口超时、网关断连、前端转圈圈全都从这儿来。所以不是“加个-Xmx4g就能糊过去”的问题架构上就不该这么干。1.2 SXSSFWorkbook 的滑动窗口机制到底怎么工作SXSSFWorkbook 的核心思路一句话说清只把最近 N 行留在内存里超出的行刷到磁盘临时文件最后统一打包成 xlsx。这个 N 就是rowAccessWindowSize构造时传入默认 100。它内部维护一个“滑动窗口”你可以理解成流水线上的一小段传送带。你createRow(1000)的时候如果当前窗口里最老的行是第 900 行那么 900 号及之前的行会被序列化成 XML 片段写进临时文件同时从内存里的Sheet对象中移除。注意是“移除引用并序列化”不是丢弃数据——最终write()时 POI 会把磁盘上的片段和内存里剩下的行按顺序拼起来再压成 zip 包。这个机制带来两个必须记住的约束很多人第一次用就栽在这儿只能顺序追加不能回头改。已经被刷盘的行你再去getRow()拿到的是 null。所以“先写一遍再回头补表头、补合计、补批注”这种玩法在 SXSSF 下行不通。行号必须递增创建。createRow(rowIndex)的 rowIndex 要单调递增不能跳着建也不能倒着建否则会抛IllegalArgumentException提示行号乱序。反过来它换来的是内存曲线的彻底改变不管导 10 万行还是 500 万行堆内存占用基本恒定在窗口大小 × 单行开销这个量级剩下的都堆在磁盘上。这就是为什么它能扛住百万级导出。注意SXSSFWorkbook 不是“更快的 XSSF”它是“更省内存的 XSSF”。单论 CPU 耗时因为多了序列化和磁盘 IO它反而可能比 XSSF 慢一点。你换它的理由永远是内存不是速度。1.3 分页查询与流式写入的天然契合点光换 SXSSFWorkbook 还不够。很多人的写法是一次性select * from xxx把百万条捞进List再循环塞进 Excel。结果 Excel 这边内存降下来了List那边又把内存撑爆了——问题只是从 POI 转移到了你的集合上总量没变。所以 SXSSFWorkbook 必须配分页查询两者是绑定的关系。思路是每页查 5000 条写进 Excel释放这一页的引用再查下一页。整个链路的内存峰值被限制在“一页数据 一个窗口的 Excel 行”这个范围和总数据量解耦。这是这套方案真正的价值所在也是标题里“分页查询导出”这几个字的落点。这里有个容易被忽略的细节分页查询的方式决定了这套方案能不能真正稳。用limit offset, size做深分页翻到第 200 页时数据库要扫描并丢弃前面 100 万行越导越慢最后卡在数据库上。用游标式分页where id lastId order by id limit size则每页都是索引定位翻到第几页耗时都一样。这个取舍我在第 2 章会展开讲先把结论记住导出场景几乎一定要用游标分页不要用 offset 分页。2. 方案设计与关键参数选型2.1 windowSize 到底定多少一次可算的参数推导rowAccessWindowSize是最常被问的参数。官方默认 100很多人直接抄然后疑惑“为什么我设了 100 内存还是涨”。先厘清一件事窗口大小限制的是留在内存里的行数不是总内存。所以内存峰值 ≈windowSize × 单行对象开销 单页数据集合开销 固定开销。拿一个具体例子算。假设每行 20 列其中 10 列是中等长度字符串平均 30 字符单行对象开销按 2KB 估windowSize 100100 × 2KB 200KB可以忽略不计windowSize 1000约 2MB仍然很小windowSize 10000约 20MB开始有感那是不是越小越好不是。窗口太小会导致刷盘过于频繁每写几行就要做一次序列化和文件 IOCPU 和磁盘都在做无用功。实测下来100 到 1000 是比较舒服的区间windowSize内存增量估算刷盘频率适用场景100默认约 200KB很高内存极度紧张、列数很多500约 1MB较高通用导出稳妥选择1000约 2MB中等大多数业务导出推荐5000约 10MB低追求吞吐、堆内存充裕2000040MB 以上很低不推荐省内存的意义被削弱我自己的默认值是 500只有在明确知道单行特别宽比如 50 列以上、含大量长文本时才降到 100在导出任务跑在独立线程池、堆内存给得比较足时提到 1000。这个值不需要精调到个位数量级对了就行。实操提示windowSize 是在构造new SXSSFWorkbook(n)时固定的运行期改不了。如果同一套代码要服务“小数据量快速导出”和“大数据量慢速导出”两种场景可以按预估总量动态选择构造参数比如预估超过 20 万行就用 200否则用 1000。2.2 分页大小的取舍别让数据库成为新瓶颈分页大小pageSize和 windowSize 是两个独立的参数前者控制“一次从数据库捞多少”后者控制“Excel 内存留多少行”。pageSize 的选型逻辑不一样它主要受三个因素约束单页数据的内存占用。5000 行 × 每行对象 1KB含业务字段 5MB 左右加上 MyBatis 结果集映射的中间对象峰值大概 10MB 到 15MB可接受。pageSize 提到 5 万单页就上百 MB等于把 OOM 从 Excel 挪到了结果集。数据库单次查询耗时。pageSize 太小比如 100百万行要查一万次网络往返和 SQL 解析的开销会盖过实际数据处理时间整体耗时反而上升。事务与一致性。一次查询越大持有的数据库连接时间越长长事务对数据库不友好。综合下来2000 到 10000 之间是甜点区我更倾向 5000。这个数字不是玄学它让百万行只需要 200 次查询连接占用时间可控单页内存也牢牢压在 10MB 量级。再说游标分页的实现。假设业务表主键是自增id导出条件是“某个时间区间内的明细”写法是select id, order_no, customer_name, amount, created_at from biz_detail where created_at #{startTime} and created_at #{endTime} and id #{lastId} order by id limit #{pageSize}每次取完一页把本页最后一条的id记下来作为下一页的lastId。第一页传lastId 0或最小值。这个写法有两个好处走主键索引翻到第几页耗时都一样不依赖offset数据在导出过程中有新增也不会导致漏行或重复行前提是新数据的 id 更大符合自增特性。如果业务主键不是自增的那就退一步用“时间 主键”的复合游标或者干脆用数据库的流式结果集比如 JDBC 的setFetchSize配合只进结果集。后者更彻底但对连接和事务的管理要求更高团队里没经验的话容易踩坑分页游标方案的可控性更好。2.3 临时文件、压缩与磁盘占用SXSSFWorkbook 把溢出的行写到磁盘临时文件默认走java.io.tmpdir。这里有两个现实问题需要注意。第一是磁盘空间。临时文件的大小和最终 xlsx 差不多甚至更大因为是未压缩的 XML 片段。导出 100 万行、20 列的明细临时文件可能达到几百 MB。如果应用容器的/tmp挂的是很小的内存盘有些基础镜像默认 tmpfs 只有几十 MB导出到一半就会抛IOException: No space left on device而且报错信息往往看不出是磁盘问题排查起来很绕。解决办法是显式指定临时目录指到空间充裕的挂载点。可以通过 JVM 启动参数调整全局临时目录也可以在有条件时给导出任务单独配置目录。更稳妥的做法是开启临时文件压缩SXSSFWorkbook workbook new SXSSFWorkbook(500); workbook.setCompressTempFiles(true);开启后临时文件以 gzip 形式写入磁盘占用能降到原来的几分之一到十几分之一代价是每次刷盘多一点 CPU。对导出这种 CPU 相对空闲、磁盘 IO 是瓶颈的场景这个交换非常划算我基本是默认打开的。第二是临时文件清理。这是最容易出事的点SXSSFWorkbook在写完之后必须调用dispose()或者close()它才会删掉那些临时文件。如果代码在中途抛异常直接跳出没走到dispose()临时文件就留在磁盘上了。一天几百次失败的导出能把磁盘塞满然后引发一连串莫名其妙的故障。所以这套代码必须写成try-finally或 try-with-resources 的形式把清理动作放在 finally 里这一点没有商量余地。2.4 依赖版本与 API 差异POI 3.x 和 4.x/5.x 在 SXSSF 相关 API 上有一些差异混用示例代码容易编译不过。几个高频差异点场景旧写法3.x新写法4.x/5.x设置日期单元格cell.setCellValue(Date)推荐cell.setCellValue(LocalDateTime)关闭工作簿workbook.close()workbook.close()内部会触发临时文件清理列宽自适应sheet.autoSizeColumn(i)需先sheet.trackAllColumnsForAutoSizing()且性能差单元格样式数量上限约 64000同样有上限必须复用版本选择上建议统一到同一个大版本不要一个模块引 3.17、另一个引 5.2类加载冲突排查起来非常痛苦。另外poi-ooxml会传递依赖poi、commons-compress、xmlbeans等如果项目里已经有其他组件引了不同版本的commons-compress可能出现NoSuchMethodError这类问题在导出功能上线前最好用依赖树命令扫一遍。3. 核心代码实现从分页游标到落盘3.1 分页数据源的抽象让导出逻辑与业务解耦导出功能做多了会发现不同业务的导出代码长得几乎一样差异只在“查什么数据”和“每一行怎么填”。所以第一步是把数据源抽象出来导出主流程只依赖一个“翻页取数”的接口public interface PageDataSourceT { /** * 按游标取下一页 * param lastCursor 上一页最后一个游标值首页传初始值 * param pageSize 每页条数 * return 本页数据返回空集合表示没有更多 */ ListT nextPage(long lastCursor, int pageSize); /** 从一条记录中取出游标值 */ long cursorOf(T record); }这个接口很薄但价值在于分页查询的 SQL、索引、游标字段全被关在实现类里导出主流程不需要知道数据来自 MySQL 还是别的存储也不需要知道游标是 id 还是时间戳。后面要做“导出任务异步化”“导出进度上报”改的都是主流程业务实现不用动。对应的实现类大概长这样MyBatis 版本public class DetailPageDataSource implements PageDataSourceDetailDTO { private final DetailMapper mapper; private final LocalDateTime start; private final LocalDateTime end; public DetailPageDataSource(DetailMapper mapper, LocalDateTime start, LocalDateTime end) { this.mapper mapper; this.start start; this.end end; } Override public ListDetailDTO nextPage(long lastCursor, int pageSize) { return mapper.selectByCursor(start, end, lastCursor, pageSize); } Override public long cursorOf(DetailDTO record) { return record.getId(); } }注意接口里nextPage返回的是List主流程处理完这一页后不要把它挂在任何成员变量上让它自然变成垃圾否则分页省下来的内存又会被“历史页引用”吃掉。这是很隐蔽的一个坑尤其在导出逻辑被包在某个Service单例里时。3.2 导出主流程骨架一个能直接抄的模板主流程要做四件事创建 workbook、写表头、循环翻页写数据、落盘并清理。骨架如下public void export(OutputStream out, PageDataSourceDetailDTO source, int pageSize, int windowSize) throws IOException { SXSSFWorkbook workbook new SXSSFWorkbook(windowSize); workbook.setCompressTempFiles(true); int rowIndex 0; try { SXSSFSheet sheet workbook.createSheet(明细); // 列宽一次性设好不要用 autoSizeColumn int[] widths {12, 24, 20, 14, 22}; for (int i 0; i widths.length; i) { sheet.setColumnWidth(i, widths[i] * 256); } // 表头样式只创建一次 CellStyle headerStyle buildHeaderStyle(workbook); CellStyle bodyStyle buildBodyStyle(workbook); // 表头 Row header sheet.createRow(rowIndex); String[] titles {ID, 订单号, 客户名称, 金额, 创建时间}; for (int i 0; i titles.length; i) { Cell cell header.createCell(i); cell.setCellValue(titles[i]); cell.setCellStyle(headerStyle); } // 分页循环 long lastCursor 0L; int total 0; while (true) { ListDetailDTO page source.nextPage(lastCursor, pageSize); if (page null || page.isEmpty()) { break; } for (DetailDTO dto : page) { Row row sheet.createRow(rowIndex); fillRow(row, dto, bodyStyle); } lastCursor source.cursorOf(page.get(page.size() - 1)); total page.size(); page.clear(); // 主动释放避免引用滞留 lastCursor lastCursor; // 游标推进见下方说明 } workbook.write(out); out.flush(); } finally { workbook.dispose(); // 关键清理临时文件 workbook.close(); } }几个必须解释的点列宽用setColumnWidth而不是autoSizeColumn。autoSizeColumn需要把整列数据读出来算宽度在 SXSSF 下你得先trackAllColumnsForAutoSizing()而且已经被刷盘的行还是算不到结果就是宽度不准 性能极差。手工设宽度的效果反而更稳定256 是 Excel 列宽的基本单位宽度值 × 256就是像素级近似。样式在循环外创建。xlsx 格式对单元格样式数量有上限约 64000 个每次循环 new 一个CellStyle很快就会撞线报IllegalStateException: The maximum number of cell styles was exceeded。这个错误在测试环境数据量小几乎不会出现一上生产就爆属于典型的“只在量大时暴露”的问题。page.clear()不是必须的但是个好习惯。如果page变量在循环结束后还被引用着比如被 lambda 捕获GC 就不会回收它。主动 clear 一下把那点不确定性掐掉。3.3 样式复用与单元格类型避开数字和日期的经典坑fillRow里最容易被低估的是单元格类型。Excel 是强类型的你塞进去的是字符串还是数字决定了它在 Excel 里能不能求和、能不能排序、排序是按字典序还是按数值。常见错误有三个第一金额用double塞进去。double有精度问题0.1 0.2 那种误差会原样带到 Excel 里财务对账时对不上。正确做法是用BigDecimal保留两位小数后转成字符串或者转成 long 存最小货币单位。如果一定要用数值单元格至少cell.setCellValue(value.doubleValue())之前先setScale(2, RoundingMode.HALF_UP)。第二长数字被显示成科学计数法。订单号、身份证号、银行卡号这类“看起来是数字其实是标识”的字段如果用数值单元格写入Excel 会自动按数值显示19 位订单号会变成1.23457E18精度还丢了。这类字段一律用setCellValue(String)写文本。第三日期格式。LocalDateTime直接写进去会变成一串数字需要在样式里配dataFormatprivate CellStyle buildBodyStyle(SXSSFWorkbook workbook) { CellStyle style workbook.createCellStyle(); style.setVerticalAlignment(VerticalAlignment.CENTER); // 数字格式两位小数千分位 style.setDataFormat(workbook.createDataFormat().getFormat(#,##0.00)); return style; } private CellStyle buildDateStyle(SXSSFWorkbook workbook) { CellStyle style workbook.createCellStyle(); style.setDataFormat( workbook.createDataFormat().getFormat(yyyy-mm-dd hh:mm:ss)); return style; }注意 POI 的日期格式串里mm的语义是有歧义的出现在hh后面时表示分钟出现在年份后面表示月这个行为跟 Excel 一致。如果想要绝对明确用yyyy\-mm\-dd\ hh:mm:ss这种转义写法更稳。还有一个实操细节SXSSFCellStyle创建之后不能再修改改了也不会生效因为样式一旦被使用就被序列化了。要改只能新建一个样式对象。所以样式规划要在写数据之前想清楚“先随便写写、最后统一美化”这条路在 SXSSF 下走不通。3.4 Web 层输出流式响应与文件名编码如果是从 HTTP 接口直接下载注意几点。响应头必须在写数据之前设置完一旦开始往OutputStream写字节再改 header 就无效了response.setContentType( application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(UTF-8); String fileName URLEncoder.encode(明细导出.xlsx, UTF-8).replace(, %20); response.setHeader(Content-Disposition, attachment; filename\export.xlsx\; filename*UTF-8 fileName);中文文件名必须做 URL 编码而且建议用filename*UTF-8这种 RFC 5987 的写法兼容性最好。只写filename的话某些浏览器会把中文解码成乱码。另一个容易被忽略的问题是异常处理。SXSSF 方案里如果导出到第 60% 时数据库连接断了异常抛出来此时响应头已经发出去、部分字节已经写给了客户端服务端没法再返回一个 JSON 错误体客户端拿到的是一个残缺的 xlsx打开会提示“文件已损坏”。这个体验很差而且用户不知道为什么失败。有两种处理策略按数据量选小数据量10 万行以内先写到内存的ByteArrayOutputStream或临时文件全部成功后再一次性响应。失败时还能返回标准错误结构。代价是多占一份内存或磁盘。大数据量只能流式输出但要保证“一旦开始写就不抛业务异常”把可能失败的查询前置比如先查一次总数和权限校验。真出异常时至少记好日志并在响应头里带一个自定的错误标记前端拦截提示“导出中断请稍后重试”。我一般会在接口里先做一个轻量的count校验或者用游标探一条数据把“没有数据”“超出单次导出上限”这类情况提前拦掉真正进入写流之后就只有技术性异常了。另外导出行数存在硬上限单个 sheet 最多 1048576 行。百万行级别正好卡在边界上所以必须有按行数切 sheet的逻辑int maxRowsPerSheet 1000000; if (rowIndex maxRowsPerSheet) { sheet workbook.createSheet(明细 (sheetNo)); rowIndex 0; // 重新写表头 }注意切 sheet 之后行号要从 0 重新开始而且 SXSSF 允许创建多个 sheet新 sheet 的行号独立计数这个不冲突。4. 性能调优与线上观测4.1 内存与耗时实测数字比描述更有说服力我做过一组对比同一份数据20 列字符串为主同一台机器4 核 8G堆 1G三种方案方案10 万行内存峰值100 万行内存峰值100 万行耗时XSSFWorkbook 全量约 380MBOOM—SXSSFWorkbook 全量 List约 420MBOOM—SXSSFWorkbook 分页游标约 150MB约 210MB约 95 秒几个观察值得说。第一SXSSF 配全量 List 的内存反而比 XSSF 高因为两边的开销叠加了这也是很多人换了 SXSSF 却没解决问题的原因。第二分页方案的内存曲线非常平10 万到 100 万只涨了 60MB那部分涨幅主要来自共享字符串表和 zip 打包缓冲跟数据行数不是线性关系。第三耗时的大头在数据库查询和磁盘 IOPOI 本身不是瓶颈所以想进一步优化优先优化 SQL 和索引而不是去调 POI 参数。4.2 GC 与临时文件清理的监控临时文件清理这件事光靠代码自觉是不够的得加监控。我的做法是在导出任务前后统计临时目录下poi-sxssf-*.tmp的文件数量如果发现数量只增不减说明有地方漏了dispose()。Path tmpDir Paths.get(System.getProperty(java.io.tmpdir)); try (StreamPath files Files.list(tmpDir)) { long count files.filter(p - p.getFileName().toString().startsWith(poi-sxssf)) .count(); log.info(当前临时文件数量: {}, count); }这段建议放在定时任务里跑每天扫一次超过阈值告警。别等到磁盘满了才发现那时候清理起来很被动。还要注意一点dispose()之后再调write()会报错所以顺序别写反。close()在较新版本里会连带清理但显式调用dispose()更保险两者都调也不会出问题内部有幂等处理。4.3 并发导出别让十个用户同时点下载导出是典型的“重资源、低频次”操作。如果不加限制十个运营同时点“导出全部”就是十个线程各自往磁盘写临时文件、各自占着一份数据库连接、各自消耗一份内存。这时候 windowSize 调得再小也没用因为总开销是并发的倍数。我的做法是给导出配一个独立的小线程池核心线程数 2 到 4队列有界队列满了直接返回“当前导出任务较多请稍后重试”。这样既保护了应用本身也不至于让用户排一小时队。同时每次导出前做一次“单次导出上限”校验比如超过 50 万行引导用户缩小时间范围把不可能的任务挡在门外。另外记得导出任务不要跑在请求线程里做长时间同步阻塞尤其是接口层有网关超时常见 30 秒或 60 秒的情况下。超过这个量级就走异步任务提交后立刻返回任务 ID后台慢慢导导完落对象存储或临时目录前端轮询进度再下载。这套改造是把导出做稳的关键一步但很多团队会拖到出事故才做。5. 常见问题排查速查表5.1 典型异常与对应处置下面这些是我在实际项目里真实遇到过、或者帮别人排查过的按出现频率排序现象 / 异常信息根因处置方式OutOfMemoryError: Java heap space数据一次性进 List或 windowSize 过大改游标分页windowSize 降到 500 以下No space left on device临时文件目录空间不足指定临时目录到容量大的挂载点开启setCompressTempFiles(true)IllegalArgumentException: Attempting to write a row[xxx]行号乱序或重复创建检查createRow的索引是否严格递增The maximum number of cell styles was exceeded循环内反复 new CellStyle样式提到循环外全局复用导出文件打不开提示损坏流未正确 flush/close或中途异常中断检查 finally 中的 close小数据量改为先落临时文件再输出中文文件名乱码未做 URL 编码用filename*UTF-8编码订单号变成科学计数法长数字用数值单元格写入改用setCellValue(String)导出速度越来越慢用了 offset 深分页换成where id lastCursor的游标分页NullPointerException在 getRow 处读取已刷盘的行SXSSF 不可回读改为写入前准备好数据磁盘上残留大量.tmp文件异常路径未执行 dispose迁移到 try-finally加临时文件数量监控5.2 排查思路先定位是哪一层在吃内存遇到 OOM 别急着改 POI 参数。正确的顺序是先 dump 堆看占用最大的对象是什么。如果是XSSFCell/XSSFRow占大头说明 Excel 模型的问题调 windowSize 或检查是否误用了 XSSF如果是ArrayList$Node或者业务 DTO 占大头说明是分页没做好数据全堆在集合里如果是byte[]占大头那多半是共享字符串表或者 zip 缓冲得从列数和字符串长度上想办法。这个定位步骤能省掉大量瞎试的时间。我见过有团队一上来就把 windowSize 从 100 改成 10结果内存没降耗时涨了三倍最后发现是数据源那边全量查询没改。另一个常见误区是把“接口超时”当成“导出太慢”。实际上 SXSSF 导出百万行也就一分多钟超时往往是因为同步接口的网关限制。这种问题改代码解决不了得改成异步任务模式。5.3 几个用血换来的实操心得第一条先估总量再开工。导出接口进来之后先用一个轻量的 count 或者游标探一条判断数据量级。10 万以内可以走同步、内存缓冲超过 10 万强制走异步任务超过 50 万必须限制时间范围。这个判断放在最前面能挡掉 80% 的事故。第二条表头样式和日期格式提前定死。SXSSF 的样式一旦使用就不可改很多人习惯“先跑通再美化”结果发现美化不上去只能重写。我现在的习惯是先在一个小 demo 里把列宽、表头底色、边框、数字格式全部调好截图确认再往正式代码里搬。第三条给导出加一个“行数进度”日志。每写完一页打一条已导出 xxxx 行线上排查问题时这一条日志价值极高能立刻判断是卡在数据库查询还是卡在写文件。第四条不要把导出逻辑塞进已有的业务 Service。它是个重资源操作需要独立的线程池、独立的超时配置、独立的监控。混在一起出问题时你会很难判断是业务慢还是导出慢。最后再分享一个小技巧如果业务方经常要“导出一部分看看”与其每次跑全量不如在接口上支持“最多导 N 行”的参数默认 5000需要全量时显式传 0。这个默认值能挡掉大部分无意义的全量导出对系统的保护比任何参数调优都直接。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑