在很多实际生产环境中,我们的 Java 服务并不是“豪华配置”:
- JVM 内存只有 512MB / 1GB
- 但客户端可能上传 几百 MB 甚至数 GB 的压缩包
- 上传后还需要 解压,并把文件上传到 MinIO
如果处理不当,很容易:
- OOM
- Full GC
- 请求阻塞、超时
- 磁盘或 CPU 被打爆
本文将分享一套低内存、可扩展、生产可用的解决方案。
一、典型错误做法(千万别这样)
❌ 1. 一次性读入内存
byte[] bytes = multipartFile.getBytes(); // 大文件直接 OOM
❌ 2. 解压到内存再上传
ZipFile zipFile = new ZipFile(file); zipFile.getInputStream(entry).readAllBytes();
❌ 3. HTTP 线程中同步解压 + 上传
- 请求线程被占用
- 客户端容易超时
- 服务吞吐量极低
二、正确思路:流式处理 + 异步管道
核心原则只有一句话:
任何时候都不要把完整文件加载进内存
目标流程如下:
客户端上传 ZIP
↓
流式写入临时文件(磁盘)
↓
后台异步解压
↓
逐个文件流式上传到 MinIO
↓
清理临时文件
整个过程内存占用稳定在 几十 MB 以内。
三、整体技术选型
| 环节 | 技术 |
|---|---|
| 文件接收 | MultipartFile.getInputStream() |
| 解压 | ZipInputStream |
| 上传 MinIO | MinioClient.putObject(InputStream) |
| 线程模型 | 异步线程池 |
| 缓冲区 | 8KB~64KB |
四、上传 ZIP:先落盘,不进内存
Controller 示例
@PostMapping("/uploadZip")
public ResponseEntity<String> uploadZip(@RequestParam("file") MultipartFile file) {
try {
Path tempZip = Files.createTempFile("upload-", ".zip");
Files.copy(file.getInputStream(), tempZip, StandardCopyOption.REPLACE_EXISTING);
// 异步处理,立刻返回
processZipAsync(tempZip);
return ResponseEntity.ok("Upload received, processing async.");
} catch (IOException e) {
return ResponseEntity.status(500).body(e.getMessage());
}
}
为什么要先写磁盘?
- 防止 HTTP 中断导致数据丢失
- 后台任务可重试
- 解压失败不影响上传接口稳定性
五、异步解压并流式上传到 MinIO
核心解压 + 上传逻辑
private void unzipAndUpload(Path zipPath) throws Exception {
try (InputStream fis = Files.newInputStream(zipPath);
ZipInputStream zis = new ZipInputStream(fis)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.isDirectory()) continue;
minioClient.putObject(
PutObjectArgs.builder()
.bucket("my-bucket")
.object(entry.getName())
.stream(zis, -1, 10 * 1024 * 1024)
.contentType("application/octet-stream")
.build()
);
zis.closeEntry();
}
}
}
关键点说明
ZipInputStream按文件流式解压zis直接作为 MinIO 的输入流- 不需要中间 byte[]
- MinIO SDK 自动分块上传
六、异步线程池(非常重要)
千万不要用 CompletableFuture.runAsync() 默认线程池。
@Bean
public Executor fileExecutor() {
return new ThreadPoolExecutor(
2,
4,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
使用方式:
executor.execute(() -> unzipAndUpload(zipPath));
这样可以:
- 控制 CPU / IO 并发
- 防止文件多时拖垮服务
七、MinIO 上传参数建议
.stream(inputStream, -1, 10 * 1024 * 1024)
| 参数 | 说明 |
|---|---|
-1 | 流长度未知 |
10MB | 分块大小,兼顾内存与吞吐 |
内存占用 ≈ 分块大小 + 少量缓冲
八、内存占用评估(真实可控)
| 模块 | 内存 |
|---|---|
| 上传缓冲 | ~8MB |
| 解压缓冲 | ~8MB |
| MinIO 分块 | ~10MB |
| JVM 其他 | ~50MB |
| 总计 | < 100MB |
👉 即使 512MB JVM 也完全可跑。
九、进阶优化方案
1️⃣ 不落地 ZIP(极限省磁盘)
ZipInputStream zis = new ZipInputStream(request.getInputStream());
⚠️ 风险:
- 失败不可重试
- 适合内网、稳定链路
2️⃣ 防 Zip Bomb(安全必做)
- 限制解压文件数
- 限制单文件大小
- 限制总解压大小
3️⃣ 目录结构映射
objectName = uploadId + "/" + entry.getName();
防止文件名冲突。
十、总结
推荐架构
✅ 流式上传
✅ 磁盘中转
✅ 异步解压
✅ MinIO 流式上传
✅ 严格限制并发
一句话总结
大文件 ≠ 大内存,关键是“流”而不是“量”
