导出10万行Excel把服务打挂-流式写入的账单

发布时间:2026/10/13 10:19:00
导出10万行Excel把服务打挂-流式写入的账单
导出 10 万行 Excel 把服务打挂流式写入的账单导出按钮点下去十万行数据要变成一个 Excel。运气好接口转圈半分钟用户骂一句慢。运气差整个服务的接口跟着卡最后 OOM 重启别人的请求也被拖下水。炸的不是 Excel 功能是内存账单。这笔账算明白流式写入就不用背看懂就会用。结论先放这儿行数过万导出就该走流式写入把内存账单的计价单位从「行数」改成「窗口」。内存账单怎么算用 POI 的常规写法XSSF导出代码直观得像在做填空题建行、建单元格、填值。问题在这些对象去了哪。每一行变成一个 Row 对象每个格子变成一个 Cell 对象每个值背后还有字符串全部驻留在堆里直到最后一步才一次性写进文件。十万行乘以十列就是一百万个 Cell 对象。算一笔粗账一个 Cell 连对象头带字段引用带字符串几十字节起步一百万个就是几十上百 MB 的纯对象膨胀这还不算字符串本身的体积和 GC 扫描的成本。堆开小了直接 OOM堆开大了只是换一种死法Full GC 越来越慢服务全体变卡。流式滑动窗口窗口里的 100 行写出后释放下一批 100 行临时文件变大堆不变一把梭全量驻留第 1 行对象第 2 行对象十万行全部驻留堆里最后才写文件事故链十万行变对象堆膨胀GC 越来越频繁所有接口一起变慢OOM 或服务假死这条链上最坑的是第四环。导出是单个接口的事卡死的却是整台机器的服务因为堆是大家共用的。代价的承担者不是点导出的人是碰巧同时在用系统的其他人。流式写入省的是什么流式写入的代表是 POI 的 SXSSF 和 EasyExcel。机制用一句话讲完只让一小窗行留在内存里。SXSSF 默认保留 100 行在窗口行写出去就从内存释放Excel 的 XML 先落临时文件最后打包成 xlsx。内存账单的计价单位从「行数乘对象大小」变成「窗口大小乘对象大小」十万行和一千万行的内存占用几乎一样。下面用一段 Java 演示两种策略的内存形状。直接运行java MemoryBill.javaJDK 11真实 POI 的对象比这里肥得多形状不变。// 直接运行java MemoryBill.java// 演示两种导出策略的内存形状。真实 POI 的对象更肥这里只看形状。// 预期输出数值随 JVM 略有浮动// 全量驻留 100000 行2 MB - 62 MB60 MB// 滑动窗口 100 行2 MB - 3 MB1 MB累计写出 100000 行// 结论账单的计价单位从行数变成了窗口。importjava.util.ArrayList;importjava.util.List;publicclassMemoryBill{staticclassRow{String[]cells;Row(intcols){cellsnewString[cols];for(inti0;icols;i){cells[i]字段内容_i;}}}staticlongusedHeapMB(){RuntimerRuntime.getRuntime();return(r.totalMemory()-r.freeMemory())/(1024*1024);}publicstaticvoidmain(String[]args)throwsException{introws100_000,cols10;// 策略一全量驻留POI 一把梭的形状System.gc();longbeforeusedHeapMB();ListRowsheetnewArrayList();for(inti0;irows;i){sheet.add(newRow(cols));}longafterusedHeapMB();System.out.printf(全量驻留 %d 行%d MB - %d MB%d MB%n,rows,before,after,after-before);// 策略二滑动窗口流式写入的形状只留最近 100 行sheetnull;System.gc();beforeusedHeapMB();ListRowwindownewArrayList(100);longwritten0;for(inti0;irows;i){window.add(newRow(cols));if(window.size()100){writtenwindow.size();window.clear();// 写出去的行从内存释放真实场景先落临时文件}}afterusedHeapMB();System.out.printf(滑动窗口 100 行%d MB - %d MB%d MB累计写出 %d 行%n,before,after,after-before,written);System.out.println(结论账单的计价单位从行数变成了窗口。);}}记住一句话导出的内存账单按行计价流式写入把计价单位改成了窗口。下载链路上的两个呼应生成只是前半段文件送到浏览器还有一段路。两个坑在前面的系列里讲过在这里换个身份回来。一是反代缓冲nginx 默认会把响应攒着往下传大文件下载要么关缓冲要么换异步方案否则用户端看起来就是「点了没反应」。二是超时十万行生成七八分钟很正常同步接口扛不住拆成「提交导出任务加查询下载链接」两个工具就是超时篇讲过的老套路。原则就一条行数过万导出就该是任务不该是请求。适用边界这一篇只管单机导出的内存账单。分库分表的分布式导出、导出格式的兼容性坑、以及临时文件的磁盘配额是更大的工程。SXSSF 的窗口大小和 EasyExcel 的批次参数以各自官方文档为准。自查的办法留一个出 OOM 时看堆 dump 里是不是成片的 Cell 和 char[]是的话这篇就是病因。判断标准也留一句单次导出行数过万、且不想让堆跟着行数涨就用流式行数在几千以内、追求代码直观XSSF 一把梭完全够用。