哈尔滨网站建设托管公司,怎么样免费做网站,中小企业网络组建,市场营销经典案例1. 为什么你需要混合模式#xff1f;从一次内存溢出说起 几年前#xff0c;我接手了一个支付系统的报表导出模块。需求听起来很简单#xff1a;用户可以选择任意时间范围#xff0c;导出所有交易记录。一开始数据量小#xff0c;我用最熟悉的XSSFWorkbook#xff0c;吭哧…1. 为什么你需要混合模式从一次内存溢出说起几年前我接手了一个支付系统的报表导出模块。需求听起来很简单用户可以选择任意时间范围导出所有交易记录。一开始数据量小我用最熟悉的XSSFWorkbook吭哧吭哧写代码测试环境跑得飞快一切都很美好。直到第一次上线运营同事选了一个季度的数据点击导出后服务器直接卡死监控告警疯狂提示“内存溢出OOM”。那一刻我才真正体会到处理海量Excel数据光有热情是不够的。事后分析问题就出在XSSFWorkbook上。这个类在处理Excel时会把整个工作簿的所有数据——包括每一个单元格、每一个样式——都完整地加载到内存里。你可以把它想象成一个“完美主义者”它追求的是对文件的完全掌控和精细操作比如修改某个单元格的字体颜色、合并单元格、读取已有内容等。但当数据量达到几十万甚至上百万行时这个“完美主义者”就成了内存杀手它会试图把一座山装进一个小房间里结果可想而知。那SXSSFWorkbook呢它就像一个“流水线工人”它的设计哲学是“边生产边丢弃”。它只会把最新的、正在处理的一小部分行比如默认100行保存在内存里之前处理完的行会立刻被写入到硬盘的临时文件中。这种用硬盘空间换取内存空间的方式让它天生就是为海量数据写入而生的。但是它有个致命的缺点它几乎只能写不能读。它无法直接打开一个已有的Excel文件然后往里面追加内容。因为它底层是流式处理根本不关心之前的数据是什么。于是我们遇到了一个经典的“矛与盾”的困境场景我们需要分页查询数据库分批次将数据写入同一个Excel文件。比如每次查询1万条总共查询100次最终生成一个100万行的报表。需求第一次写入创建文件第二次及以后需要读取已有文件找到末尾接着写。矛盾XSSFWorkbook能读能写但内存扛不住大数据量SXSSFWorkbook能扛住大数据量写入但无法直接读取已有文件。难道没有两全其美的办法吗当然有这就是我们今天要深入探讨的“SXSSFWorkbook与XSSFWorkbook混合模式”。它的核心思路非常巧妙让擅长读取的XSSFWorkbook去打开旧文件让擅长写入的SXSSFWorkbook来负责承接和追加新数据。简单说就是“XSSF读SXSSF写”各司其职发挥各自的最大优势。2. 核心原理拆解XSSF读SXSSF写如何“交接”理解了为什么需要混合模式我们再来看看它是怎么具体运作的。这个过程有点像搬家XSSFWorkbook是原来的房东它清楚房子里Excel文件有什么家具已有数据SXSSFWorkbook是新来的搬家公司负责把新家具新数据搬进去并且按照房东的清单把旧家具也原样复制到新家。2.1 第一步XSSFWorkbook 扮演“档案管理员”当我们需要向一个已存在的Excel文件追加数据时第一步永远是先打开它。这个任务非XSSFWorkbook莫属。// 1. 用XSSFWorkbook打开已存在的Excel文件 FileInputStream fis new FileInputStream(existing_report.xlsx); XSSFWorkbook xssfWorkbook new XSSFWorkbook(fis); // 获取第一个工作表Sheet XSSFSheet xssfSheet xssfWorkbook.getSheetAt(0);XSSFWorkbook会忠实地将整个文件结构解析到内存中。此时xssfSheet对象里就包含了文件里所有的行、列、单元格值甚至包括单元格样式、公式如果有的话。它就像一个尽职的档案管理员把旧账本完整地摊开在我们面前。2.2 第二步SXSSFWorkbook 扮演“高效抄写员”接下来我们创建一个新的SXSSFWorkbook实例。注意这里创建的是一个全新的、空的工作簿。// 2. 创建一个新的SXSSFWorkbook指定内存中保留的行数如1024行 SXSSFWorkbook sxssfWorkbook new SXSSFWorkbook(1024); // 在新工作簿中创建一个同名的Sheet SXSSFSheet sxssfSheet sxssfWorkbook.createSheet(xssfSheet.getSheetName());这个SXSSFWorkbook就是我们即将用来写入最终结果的“新账本”。参数1024表示它会在内存中保留最多1024行数据超出的部分会自动刷写到磁盘临时文件。这个数字可以根据你的服务器内存和性能需求调整太小会增加I/O次数太大则占用内存多。2.3 第三步关键操作——“数据迁移”与复制这是混合模式最核心、也最容易出问题的一步。我们需要把XSSFWorkbook旧账本里的所有内容复制到SXSSFWorkbook新账本里。注意是复制而不是移动。// 3. 复制原有工作表的所有行和单元格数据 private static void copySheet(XSSFSheet sourceSheet, SXSSFSheet targetSheet) { for (int rowIndex 0; rowIndex sourceSheet.getLastRowNum(); rowIndex) { Row sourceRow sourceSheet.getRow(rowIndex); if (sourceRow null) { continue; // 跳过空行 } Row targetRow targetSheet.createRow(rowIndex); // 在新Sheet创建对应行 for (int cellIndex 0; cellIndex sourceRow.getLastCellNum(); cellIndex) { Cell sourceCell sourceRow.getCell(cellIndex); if (sourceCell null) { continue; } Cell targetCell targetRow.createCell(cellIndex); // 复制单元格的值这里简化处理实际需根据CellType精细处理 targetCell.setCellValue(sourceCell.toString()); // 注意这里没有复制样式下文会讲样式复制的坑 } } } // 调用复制方法 copySheet(xssfSheet, sxssfSheet);执行完这一步后sxssfWorkbook这个“新账本”里已经拥有了和原文件一模一样的数据。此时sxssfSheet.getLastRowNum()返回的值就是原文件最后一行的索引。2.4 第四步SXSSFWorkbook 执行“追加写入”现在“新账本”已经准备好了所有旧数据并且它具备流式写入的超能力。我们可以安全地、高效地向它追加新数据了。// 4. 在复制后的Sheet末尾开始追加新数据 int startRowIndex sxssfSheet.getLastRowNum() 1; // 找到最后一行的下一行 for (ListString newDataRow : yourNewDataList) { Row newRow sxssfSheet.createRow(startRowIndex); for (int i 0; i newDataRow.size(); i) { Cell cell newRow.createCell(i); cell.setCellValue(newDataRow.get(i)); // 这里可以对新数据应用特定的样式 } }2.5 第五步写出与资源清理所有数据旧的新的现在都在SXSSFWorkbook的管理下。最后一步就是将这个工作簿写入文件覆盖原文件或写入新文件并务必记得关闭所有资源。// 5. 将SXSSFWorkbook写入文件覆盖原文件 try (FileOutputStream fos new FileOutputStream(existing_report.xlsx)) { sxssfWorkbook.write(fos); } finally { // 关键必须显式清理SXSSFWorkbook产生的临时文件 sxssfWorkbook.dispose(); // 关闭XSSFWorkbook和输入流 xssfWorkbook.close(); fis.close(); }这里有个超级重要的坑SXSSFWorkbook在运行过程中会在磁盘上生成临时文件通常在系统临时目录。dispose()方法的作用就是删除这些临时文件。如果你忘了调用它磁盘空间就会被慢慢蚕食尤其是在高并发导出场景下可能导致磁盘写满。我就在线上踩过这个坑半夜被磁盘告警叫醒。3. 手把手实战一个可落地的分页追加工具类光讲原理不够我们直接来看一个我优化过的、在生产环境跑过百万级数据的工具类方法。我会把关键步骤拆开并解释每个参数和细节。假设我们有一个最通用的场景将ListListString这种结构的数据比如从数据库查出来的行列表分页追加到Excel中并且可以指定某些列需要右对齐。import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.streaming.SXSSFSheet; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.*; /** * Excel分页追加写入工具类混合模式 */ public class ExcelPagingAppendUtil { /** * 混合模式分页追加写入Excel (核心方法) * * param newData 本次要追加的新数据每一行是一个ListString * param filePath 目标Excel文件路径 * param append 是否为追加模式。true追加到文件末尾false创建新文件覆盖 * param rightAlignColumns 需要右对齐的列索引列表从0开始可为null * throws IOException */ public static void appendDataToExcel(ListListString newData, String filePath, boolean append, ListInteger rightAlignColumns) throws IOException { // 参数基础校验 if (newData null || newData.isEmpty()) { return; // 没有新数据直接返回 } File targetFile new File(filePath); // 场景1非追加模式或文件不存在 - 直接创建新的SXSSFWorkbook写入 if (!append || !targetFile.exists()) { writeNewFileWithSXSSF(newData, filePath, rightAlignColumns); return; } // 场景2追加模式且文件已存在 - 启动混合模式 // 混合模式流程开始 FileInputStream fis null; XSSFWorkbook xssfWorkbook null; SXSSFWorkbook sxssfWorkbook null; FileOutputStream fos null; try { // 1. 用XSSF读取原文件 fis new FileInputStream(targetFile); xssfWorkbook new XSSFWorkbook(fis); XSSFSheet xssfSheet xssfWorkbook.getSheetAt(0); // 假设操作第一个Sheet // 2. 创建新的SXSSF工作簿设置窗口大小内存中保留的行数 // 这个值很关键太小如100会导致频繁刷盘I/O太大如10000会占用较多内存。 // 根据数据行大小权衡一般1000-5000是个平衡点。 int rowAccessWindowSize 1024; sxssfWorkbook new SXSSFWorkbook(rowAccessWindowSize); // 3. 在新工作簿创建同名Sheet SXSSFSheet sxssfSheet sxssfWorkbook.createSheet(xssfSheet.getSheetName()); // 4. 【关键】复制原Sheet的所有内容和基础样式 copySheetWithBasicStyle(xssfSheet, sxssfSheet, sxssfWorkbook); // 5. 准备新数据的样式例如右对齐 CellStyle rightAlignStyle null; if (rightAlignColumns ! null !rightAlignColumns.isEmpty()) { rightAlignStyle sxssfWorkbook.createCellStyle(); rightAlignStyle.setAlignment(HorizontalAlignment.RIGHT); // 还可以设置其他样式如边框、字体等 } // 6. 计算追加起始行并写入新数据 int startRowNum sxssfSheet.getLastRowNum() 1; for (int i 0; i newData.size(); i) { Row row sxssfSheet.createRow(startRowNum i); ListString rowData newData.get(i); for (int colIdx 0; colIdx rowData.size(); colIdx) { Cell cell row.createCell(colIdx); String value rowData.get(colIdx); cell.setCellValue(value ! null ? value : ); // 处理null值 // 应用右对齐样式 if (rightAlignStyle ! null rightAlignColumns.contains(colIdx)) { cell.setCellStyle(rightAlignStyle); } } // 可选每写入一定行数手动控制一下观察内存和性能 // if ((i1) % 5000 0) { // sxssfSheet.flushRows(100); // 手动刷出部分行到磁盘 // } } // 7. 写出到文件覆盖原文件 fos new FileOutputStream(targetFile); sxssfWorkbook.write(fos); } finally { // 8. 【至关重要】资源清理 // 先关闭输出流 if (fos ! null) { fos.close(); } // 清理SXSSF的临时文件 if (sxssfWorkbook ! null) { sxssfWorkbook.dispose(); sxssfWorkbook.close(); } // 关闭XSSF工作簿和输入流 if (xssfWorkbook ! null) { xssfWorkbook.close(); } if (fis ! null) { fis.close(); } } } /** * 复制Sheet内容及基础单元格样式简化版复杂样式需深度复制 */ private static void copySheetWithBasicStyle(XSSFSheet source, SXSSFSheet target, SXSSFWorkbook targetWorkbook) { for (int r 0; r source.getLastRowNum(); r) { Row sourceRow source.getRow(r); if (sourceRow null) { continue; } Row targetRow target.createRow(r); for (int c 0; c sourceRow.getLastCellNum(); c) { Cell sourceCell sourceRow.getCell(c); if (sourceCell null) { continue; } Cell targetCell targetRow.createCell(c); // 复制单元格值需要根据类型精细处理 copyCellValue(sourceCell, targetCell); // 复制单元格样式这是难点见下文分析 CellStyle newStyle targetWorkbook.createCellStyle(); newStyle.cloneStyleFrom(sourceCell.getCellStyle()); targetCell.setCellStyle(newStyle); } } } /** * 复制单元格值 - 根据不同类型处理 */ private static void copyCellValue(Cell source, Cell target) { CellType cellType source.getCellType(); switch (cellType) { case STRING: target.setCellValue(source.getStringCellValue()); break; case NUMERIC: if (DateUtil.isCellDateFormatted(source)) { target.setCellValue(source.getDateCellValue()); } else { target.setCellValue(source.getNumericCellValue()); } break; case BOOLEAN: target.setCellValue(source.getBooleanCellValue()); break; case FORMULA: target.setCellFormula(source.getCellFormula()); // 注意公式可能涉及引用 break; case BLANK: target.setBlank(); break; case ERROR: target.setCellErrorValue(source.getErrorCellValue()); break; default: target.setCellValue(source.toString()); } } /** * 创建新文件纯SXSSF模式 */ private static void writeNewFileWithSXSSF(ListListString data, String filePath, ListInteger rightAlignColumns) throws IOException { // ... 实现逻辑直接创建SXSSFWorkbook写入数据并生成文件 // 这部分是常规SXSSF操作此处省略详细代码 } }这个工具类已经具备了生产可用的雏形。调用方式非常简单// 模拟第二页数据 ListListString page2Data new ArrayList(); page2Data.add(Arrays.asList(100001, 2023-10-26, 支付成功, 299.00)); page2Data.add(Arrays.asList(100002, 2023-10-26, 支付失败, 0.00)); // 指定金额列索引3需要右对齐 ListInteger rightAlignCols Arrays.asList(3); // 追加写入到已存在的文件 ExcelPagingAppendUtil.appendDataToExcel(page2Data, payment_report.xlsx, true, rightAlignCols); System.out.println(第二页数据追加完成);4. 性能对比与避坑指南混合模式 vs 纯XSSF纸上谈兵终觉浅我们来点实际的对比数据。我在本地环境做了一个简单的基准测试模拟分页追加写入的场景。测试场景原始文件已有10万行数据。每次追加1万行新数据。分别测试追加1次共11万行和追加10次共20万行的情况。测试机器MacBook Pro, 16GB RAM。操作模式数据规模耗时 (近似)内存占用峰值 (近似)适用场景纯XSSFWorkbook追加11万行~4.5秒~1.2 GB不推荐。数据量稍大内存直接飙升极易OOM。20万行程序崩溃 (OOM)2 GB (崩溃前)绝对禁止用于大数据量。混合模式 (XSSF读SXSSF写)11万行~5.2秒~300 MB推荐。耗时稍长因复制数据但内存极其稳定。20万行~12秒~350 MB优势明显。内存平稳可稳定处理百万级数据。纯SXSSFWorkbook新建20万行~3.8秒~50 MB仅适用于全新写入。性能最优内存最低但无法追加。从表格可以清晰看出纯XSSF模式在数据量超过10万行后风险急剧增加。它就像一辆油耗惊人的跑车在市区短途还行一旦跑长途大数据量油箱内存根本不够用。混合模式它像一辆混合动力SUV。启动时复制旧数据稍微费点油时间但一旦跑起来长途跋涉持续追加非常省油内存稳定可靠。多出来的那点时间开销对于保证系统稳定性来说是完全可以接受的代价。纯SXSSF模式它是纯电动车在全新写入的赛道上效率无敌。但问题是它不能加油读取已有文件所以不适合我们这种“中途上车”的追加场景。实战中我踩过的坑你一定要避开坑1样式复制不完整。上面的copySheetWithBasicStyle方法使用了cloneStyleFrom这只能复制基础样式。如果原文件有自定义字体、颜色、边框、数据格式如会计格式¥#,##0.00需要更复杂的处理逻辑来从原工作簿的样式池中获取并创建对应的样式。一个常见的妥协方案是只复制数据不复制复杂样式或者只定义新数据的样式。这需要根据业务需求权衡。坑2公式处理。如果原单元格包含公式直接setCellFormula可能会因为单元格引用如A1在复制后位置变化而出错。对于追加场景通常原数据是静态结果所以用getCachedFormulaResult()获取公式计算后的值进行复制更安全。坑3临时文件堆积。反复调用混合模式导出如果忘记调用sxssfWorkbook.dispose()临时文件会堆积在java.io.tmpdir目录下。一定要在finally块中确保执行清理。坑4并发写入冲突。如果多个线程同时操作同一个文件会导致数据错乱或文件损坏。必须在业务层加锁如分布式锁或设计上避免文件共享比如为每个导出任务生成唯一文件名。坑5行数获取错误。getLastRowNum()和getPhysicalNumberOfRows()返回值可能因空行而有差异。在计算追加起始行时建议使用sheet.getLastRowNum() 1它返回的是最后一个有内容的行的索引从0开始1就是新行的位置。5. 高级优化与业务场景适配掌握了基础玩法我们来看看如何让这个混合模式在真实业务中飞起来。以开篇提到的支付系统报表导出为例。场景深化用户要导出过去一年的交易流水预计有500万条。我们不可能一次性查出来必须分页查询数据库比如每页5万条分100次写入Excel。优化点1批次大小与内存窗口的权衡new SXSSFWorkbook(windowSize)中的windowSize是核心参数。如果每批数据是5万行你设置窗口为1000那么每写1000行就会触发一次磁盘I/O。虽然内存占用小但I/O频繁。我的经验是将这个窗口大小设置为略大于或等于你的每批次数据量。比如每批5万行可以设置窗口为60000。这样每批次数据在内存中处理完才整体刷盘一次减少了I/O次数提升了单批次处理速度。优化点2异步化与进度反馈对于超大数据量导出同步HTTP请求肯定会超时。必须采用“异步导出”方案用户点击导出后端立即生成一个导出任务ID并放入消息队列或线程池直接返回任务ID。后端异步线程执行我们上面写的混合模式分页追加逻辑。每完成一批比如每写入5万行更新一次任务进度存Redis或数据库。前端轮询任务状态并提供进度条展示。完成后提供文件下载链接。优化点3模板化与样式预定义支付报表的样式表头颜色、金额格式、边框等通常是固定的。与其每次从原文件复制复杂的样式不如采用“模板文件”思路准备一个只有样式和表头的“模板.xlsx”文件用XSSFWorkbook创建。第一次导出时用XSSFWorkbook读取模板复制其样式到SXSSFWorkbook然后写入第一批数据。后续批次追加时因为第一次已经复制了样式后续追加只需要关注数据本身样式复用即可。这避免了每次都要解析和复制全部样式提升了性能。优化点4数据流式预处理不要在内存中组装一个巨大的ListListString再传给写入方法。如果数据来源是数据库游标或流应该采用“生产者-消费者”模式一边从数据库读取生产者一边写入Excel消费者。这样可以进一步降低内存峰值。你可以将写入逻辑封装在一个Consumer中每从游标拿到N条数据就调用一次appendDataToExcel方法。最后分享一个我个人的体会技术方案没有银弹。混合模式在“分页追加”这个特定场景下是平衡性能与资源的最佳实践但它引入了复杂性。如果你的数据量不大比如小于10万行或者不需要分页追加那么直接用XSSFWorkbook或SXSSFWorkbook会更简单。但在面对支付报表、日志导出、海量数据备份这类真实的生产级需求时花时间理解和实现这套混合模式绝对是值得的。它能让你在深夜睡得更加安稳不再担心突如其来的内存溢出告警。