You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在内存生成Excel并流式上传至S3?规避JVM内存溢出与磁盘写入

流式生成Excel并分片上传至S3(无磁盘写入+低内存)

这个方案完全可行,我和不少同行都在大数据处理场景里落地过类似逻辑,核心是把POI的流式输出和S3的分片上传通过内存流打通,彻底绕开磁盘写入,同时控制内存占用。

核心思路拆解

1. 分页增量读取数据集

用你提到的分页方式即可,不管是JDBC的游标分页(避免大偏移量性能问题),还是MyBatis、JPA的分页查询,每次拉取固定行数(比如1000-5000行,根据内存情况调整),处理完一批再取下一批,避免一次性加载全量数据。

2. 用内存流替代磁盘文件对接POI Streaming

Apache POI的SXSSFWorkbook确实需要输出流,但不一定是文件流——用PipedOutputStream+PipedInputStream的组合就能在内存里完成数据流转,同时避免把整个Excel文件加载到内存:

  • 单独开一个线程,用SXSSFWorkbook逐步写入数据到PipedOutputStream
  • 主线程从对应的PipedInputStream读取数据,分块传给S3的分片上传接口

3. S3分片上传的流式对接

利用AWS SDK的TransferManager或者直接调用Multipart Upload API,把从PipedInputStream读取的字节分块(每块5MB以上,符合S3分片要求),逐个上传到S3,最后完成分片合并。

关键代码示例

1. 内存流与POI的结合

// 创建管道流,需多线程处理避免阻塞
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);

// 开线程处理POI写入
new Thread(() -> {
    try (SXSSFWorkbook workbook = new SXSSFWorkbook(1000)) { // 内存保留1000行,超出自动刷到流
        SXSSFSheet sheet = workbook.createSheet("data");
        
        // 写入表头
        Row header = sheet.createRow(0);
        header.createCell(0).setCellValue("ID");
        header.createCell(1).setCellValue("Name");
        
        // 分页读取数据并写入
        int rowNum = 1;
        while (hasNextPage()) {
            List<Data> dataList = fetchNextPage(); // 你的分页查询方法
            for (Data data : dataList) {
                Row row = sheet.createRow(rowNum++);
                row.createCell(0).setCellValue(data.getId());
                row.createCell(1).setCellValue(data.getName());
            }
            workbook.flush(); // 刷出已处理的行到输出流,释放内存
        }
        
        workbook.write(pos); // 最终收尾写入
        pos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

2. 流式分片上传到S3

// 初始化S3客户端
AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();

// 初始化分片上传
InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest("your-bucket", "your-file.xlsx");
InitiateMultipartUploadResult initResult = s3Client.initiateMultipartUpload(initRequest);

List<PartETag> partETags = new ArrayList<>();
byte[] buffer = new byte[5 * 1024 * 1024]; // 5MB分片大小,符合S3要求
int bytesRead;
int partNumber = 1;

try {
    while ((bytesRead = pis.read(buffer)) != -1) {
        // 准备分片上传请求
        UploadPartRequest uploadRequest = new UploadPartRequest()
                .withBucketName("your-bucket")
                .withKey("your-file.xlsx")
                .withUploadId(initResult.getUploadId())
                .withPartNumber(partNumber++)
                .withInputStream(new ByteArrayInputStream(buffer, 0, bytesRead))
                .withPartSize(bytesRead);
        
        // 上传分片并保存ETag
        UploadPartResult uploadResult = s3Client.uploadPart(uploadRequest);
        partETags.add(uploadResult.getPartETag());
    }
    
    // 完成分片合并
    CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest("your-bucket", "your-file.xlsx", initResult.getUploadId(), partETags);
    s3Client.completeMultipartUpload(completeRequest);
} catch (IOException e) {
    // 出错时取消分片上传,避免S3留存垃圾数据
    s3Client.abortMultipartUpload(new AbortMultipartUploadRequest("your-bucket", "your-file.xlsx", initResult.getUploadId()));
    e.printStackTrace();
} finally {
    pis.close();
}

注意事项

  • 线程同步:Piped流是阻塞的,必须用单独线程处理POI写入,否则会出现死锁
  • 内存控制:SXSSFWorkbook的构造参数(内存保留行数)要根据JVM内存调整,避免OOM
  • 分片大小:S3要求分片最小5MB(最后一个分片除外),缓冲区设为5MB适配性最好
  • 异常处理:上传出错时一定要调用abortMultipartUpload取消未完成的分片

关于Excel的压缩特性:Excel本质是ZIP压缩的XML集合,SXSSFWorkbook在流式写入时会逐步生成XML并压缩,通过流传递的是已经压缩后的字节,不会在内存里留存整个未压缩的文件,完全符合低内存的要求。

内容的提问来源于stack exchange,提问作者Bobert

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 04:07:39