如何在内存生成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
相关产品推荐
相关产品推荐

