在很多实际生产环境中,我们的 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
上传 MinIOMinioClient.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 流式上传
严格限制并发

一句话总结

大文件 ≠ 大内存,关键是“流”而不是“量”

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注