当前位置: 首页 > news >正文

多人同时导出 Excel 导致内存溢出

1、问题根因分析

        多人同时导出Excel导致内存溢出(OOM)的核心原因是:在短时间内,大量数据被加载到JVM堆内存中,且创建了大量大对象(如Apache POI的Cell、Row、Sheet对象),超过了堆内存的最大限制(-Xmx)。

  1. 同步处理与高并发:导出请求通常是同步的。当多个用户同时触发导出时,每个请求都会在服务器端创建一个处理线程,并在该线程中构建一个完整的、包含大量数据的Excel工作簿(Workbook)对象。

  2. Apache POI的内存模型:传统的Apache POI(如HSSF/XSSF)在构建Excel时,所有单元格、样式、数据都保存在内存中的Java对象里。一个几十万行的Excel文件,其对应的Workbook对象可能轻松占用几百MB甚至上GB的内存。

  3. 数据一次性加载:为了生成Excel,通常需要从数据库一次性查询出所有数据,这个巨大的ResultSet也会占用大量内存。

  4. JVM堆空间不足:如果JVM堆内存设置本身不大,或者并发导出的用户数足够多,就很容易将堆内存耗尽,触发java.lang.OutOfMemoryError: Java heap space

2、解决方案(从低代价到高代价,从临时到根本)

2.1 应用层优化(代码改造 - 最根本的解决方案)

这是最推荐的方式,从根源上解决内存问题。

a) 使用流式API (SXSSF)

Apache POI提供了专门用于处理大数据量的流式API:SXSSF。

原理

SXSSF(Streaming Usermodel API)在XSSF的基础上扩展,它只会将一部分行(例如100行)保留在内存中,生成一行,刷新一行到磁盘临时文件,从而实现低内存占用。它通过滑动窗口机制来管理内存中的行。

  • 优点:内存占用极低且恒定(仅与rowAccessWindowSize有关),是解决此问题的最佳武器。

  • 缺点:不支持一些高级特性(如公式计算、单元格合并等在刷新后可能受限),会生成临时文件。

代码示例
// 设置一个滑动窗口值,表示在内存中保留多少行,超出的行会被写入磁盘
int rowAccessWindowSize = 100;
SXSSFWorkbook workbook = new SXSSFWorkbook(rowAccessWindowSize);
SXSSFSheet sheet = workbook.createSheet("数据导出");// 写入表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("姓名");
headerRow.createCell(1).setCellValue("年龄");// 流式分页查询数据库并写入
int pageSize = 1000;
int pageNum = 1;
List<Data> dataList;
int currentRow = 1;do {// 1. 分页查询,避免一次性加载所有数据dataList = dataService.getExportData(pageNum, pageSize);if (dataList.isEmpty()) {break;}// 2. 将本页数据写入Excelfor (Data data : dataList) {Row row = sheet.createRow(currentRow++);row.createCell(0).setCellValue(data.getName());row.createCell(1).setCellValue(data.getAge());// ... 其他单元格}// 3. 非常重要!手动清理掉滑动窗口之外的行,释放内存// ((SXSSFSheet)sheet).flushRows() 也可以,但注意参数((SXSSFSheet) sheet).flushRows(dataList.size()); // 刷新并清理已处理的行pageNum++;
} while (!dataList.isEmpty());// 将workbook写入HttpServletResponse的输出流
workbook.write(response.getOutputStream());
workbook.dispose(); // 删除临时文件
workbook.close();

b) 分页查询数据库

如上例所示,在数据获取层,绝对不要一次性SELECT * FROM huge_table,必须使用分页查询(如MySQL的LIMIT offset, size)。这大大降低了数据库和Java应用两边的内存压力。

2.2 架构与流程优化

a) 异步导出

将同步请求改为异步任务。

  • 流程

    • 用户点击导出后,服务端立即返回一个任务ID或一个URL:“正在生成,请稍后查看下载链接”。
    • 后台使用一个独立的、线程池大小可控的任务(如使用@Async、消息队列、Job调度)来执行真正的导出操作。
    • 导出完成后,将文件上传到OSS或文件服务器,并将下载链接通过通知系统(站内信、邮件等)告知用户,或者更新任务状态供用户查询。
  • 优点
    • 避免了HTTP请求超时。

    • 可以对后台任务队列进行控流,避免同时处理过多导出任务,从而控制内存使用峰值。

    • 用户体验更好,不会因为长时间等待而导致浏览器卡死。

  • 缺点:系统设计更复杂,不能立即下载。

b) 限流与队列

如果必须同步导出,可以在应用入口进行限流。

  • 使用RateLimiter(Guava)或Sentinel等工具,限制单位时间内允许的导出请求数量。例如,最多只允许同时处理2个导出请求,后续请求排队等待或直接返回“系统繁忙,请稍后再试”。

  • 优点:简单粗暴,有效防止系统被瞬时高并发打垮。

  • 缺点:用户体验差(需要等待或失败)。

2.3 运维与配置优化(临时缓解措施)

这些不能根治问题,但可以作为一个缓冲或辅助手段。

a) 增加JVM堆内存

通过调整启动参数 -Xmx4g -Xms4g 来增大最大堆内存。

  • 优点:简单,快速。

  • 缺点

    • 只是推迟了OOM发生的时间,如果数据量或并发量持续增长,迟早还会溢出。

    • 大内存会带来更长时间的Full GC(Garbage Collection),导致应用“卡顿”。

b) 优化GC参数

针对大内存和创建大量短命对象(导出任务中的对象基本都是短命对象)的场景,使用G1垃圾收集器可能效果更好。

  • 参数示例:-XX:+UseG1GC -XX:MaxGCPauseMillis=200

c) 文件拆分与压缩

对于极端大量的数据,可以考虑不再导出单一Excel文件,而是导出多个压缩包(如每10万行一个Excel,然后打包成ZIP)。但这更多是业务逻辑的变更。

2.4 总结

  1. 立即止损(线上紧急情况)

    • 短期:如果正在频繁OOM,可以先增大堆内存 -Xmx 并重启服务,快速恢复业务。

    • 同时:在网关/应用层紧急添加导出限流策略,防止问题复发。

  2. 根本解决(中期必须完成)

    • 改造代码:将导出逻辑从使用HSSFWorkbook/XSSFWorkbook迁移到SXSSFWorkbook

    • 优化数据查询:确保数据获取是分页的,而不是一次性加载。

  3. 优化体验与架构(长期规划)

    • 改为异步导出,并提供任务查询界面。这是对用户和最系统都最友好的方式。

    • 考虑将生成的大文件存储到OSS等对象存储中,减轻应用服务器磁盘IO压力。

技术选型参考

  • 首选SXSSF + 分页查询 + 异步导出

  • 备选:如果数据模型非常简单,也可以考虑直接生成CSV文件,CSV是纯文本格式,内存开销远小于Excel。但缺点是无法处理样式和多个Sheet。

http://www.dtcms.com/a/340562.html

相关文章:

  • Linux多线程——线程池
  • 论文见刊后能加通讯作者吗?
  • 【面试题】什么是三次握手四次挥手呢?
  • 黑盒(功能)测试基本方法详解
  • 关于删除gitlab中的分支
  • C语言:第18天笔记
  • DINOv3
  • 【Android】一文详解Android里的AOP编程
  • 专题:2025全球消费趋势与中国市场洞察报告|附300+份报告PDF、原数据表汇总下载
  • 【0基础PS】图片格式
  • LWIP的TCP协议
  • Chrome 中的 GPU 加速合成
  • Google Chrome v139.0.7258.139 便携增强版
  • IP查找的方法、工具及应用场景
  • 让Chrome信任自签名证书
  • Google Chrome 扩展不受信任 - 不受支持的清单版本 解决方案
  • 单北斗GNSS位移监测技术解析
  • 爬虫逆向--Day16Day17--核心逆向案例3(拦截器关键字、路径关键字、请求堆栈、连续请求)
  • 欧州服务器String 转 double 有BUG?
  • Ubuntu 上安装 MongoDB
  • 【数据库】Oracle学习笔记整理之六:ORACLE体系结构 - 重做日志文件与归档日志文件(Redo Log Files Archive Logs)
  • RabbitMQ:生产者可靠性(生产者重连、生产者确认)
  • 多模型创意视频生成平台
  • 超高清与低延迟并行:H.266 在行业视频中的落地图谱
  • 【嵌入式电机控制#34】FOC:意法电控驱动层源码解析——HALL传感器中断(不在两大中断内,但重要)
  • 关联查询(left/right)优化
  • 50GHz+示波器:精准捕捉超高频信号
  • 激光雷达点云平面拟合与泊松重建对比分析
  • 【ElasticSearch】ElasticSearch Overview
  • Day 40:训练和测试的规范写法