java大文件上传处理

Java 大文件上传处理(从简单到生产级完整方案)

在实际项目中,上传几百MB甚至几个GB的文件非常常见。如果直接用普通的 MultipartFile 一次性接收,会导致以下问题:

  • 内存溢出(OutOfMemoryError)
  • 请求超时(timeout)
  • 上传中断后需要重头开始
  • 服务器压力巨大

核心解决方案分片上传(Chunk Upload) + 断点续传 + 秒传

下面从零基础到生产级,一步步带你搞懂 Java 处理大文件上传的最佳实践(2024-2025 主流方案)。

一、不同场景推荐方案对比

文件大小并发量推荐方案核心技术复杂度内存占用
< 100MB普通 MultipartFileSpring Boot 默认★☆☆☆☆中等
100MB ~ 2GB流式接收 + 临时文件InputStream + FileOutputStream★★☆☆☆
> 2GB 或重要业务分片上传 + 断点续传 + 秒传前端切片 + 后端合并 + Redis/MySQL记录★★★★☆极低
云存储场景直接走云厂商分片接口OSS/S3/COS 多部分上传★★☆☆☆几乎无

绝大多数中大型项目现在都推荐第3种:分片 + 断点续传 + 秒传

二、主流实现方式(推荐生产级方案)

整体流程

  1. 前端:把大文件切成固定大小的分片(通常 5MB~20MB 一片)
  2. 前端:计算整个文件的唯一标识(MD5 / SHA256)
  3. 前端:先发一个“校验”请求 → 服务器告诉哪些分片已存在(实现秒传 & 续传)
  4. 前端:并发或串行上传缺失的分片(带分片序号、文件hash)
  5. 后端:接收分片 → 存临时目录或直接写到最终文件对应位置
  6. 前端:所有分片上传完后 → 发“合并”请求
  7. 后端:合并分片 → 校验完整性 → 转移到正式目录

三、后端核心代码(Spring Boot 实现)

1. 配置文件(非常重要!防止内存爆炸)

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10GB          # 单文件最大
      max-request-size: 10GB       # 单次请求最大
      file-size-threshold: 0       # 立即写磁盘,不占内存

2. Controller 接口(推荐写法)

@RestController
@RequestMapping("/file")
public class BigFileController {

    // 临时目录(建议配置到外部磁盘)
    private static final String TEMP_DIR = "/data/upload_temp/";
    private static final String FINAL_DIR = "/data/upload_final/";

    // 1. 校验文件是否已存在(秒传) + 返回已上传分片
    @GetMapping("/check")
    public Result checkFile(@RequestParam String fileMd5, @RequestParam Long fileSize) {
        // 查询 Redis 或 DB 是否有该 md5 的完整文件
        if (fileExistsInDb(fileMd5)) {
            return Result.ok("文件已存在,可秒传", getFileUrl(fileMd5));
        }

        // 返回已上传的分片索引列表(比如 [0,1,3,5])
        List<Integer> uploadedChunks = getUploadedChunks(fileMd5);
        return Result.ok(uploadedChunks);
    }

    // 2. 上传分片
    @PostMapping("/upload/chunk")
    public Result uploadChunk(
            @RequestParam("file") MultipartFile chunk,
            @RequestParam String fileMd5,
            @RequestParam Integer chunkIndex,     // 分片序号 0,1,2...
            @RequestParam Integer totalChunks) {

        try {
            String chunkFileName = fileMd5 + "_" + chunkIndex;
            File chunkFile = new File(TEMP_DIR + fileMd5 + "/" + chunkFileName);

            // 已存在则跳过(幂等)
            if (chunkFile.exists()) {
                return Result.ok("分片已存在");
            }

            // 流式写入磁盘(内存占用极低)
            chunk.transferTo(chunkFile);

            // 记录分片信息到 Redis(推荐)或 DB
            saveChunkInfo(fileMd5, chunkIndex);

            // 如果所有分片都齐了,自动触发合并(可选)
            if (isAllChunksUploaded(fileMd5, totalChunks)) {
                mergeChunks(fileMd5, totalChunks);
            }

            return Result.ok();
        } catch (Exception e) {
            return Result.error("上传失败:" + e.getMessage());
        }
    }

    // 3. 手动触发合并(前端所有分片上传完后调用)
    @PostMapping("/merge")
    public Result merge(@RequestParam String fileMd5, @RequestParam Integer totalChunks) {
        if (!isAllChunksUploaded(fileMd5, totalChunks)) {
            return Result.error("分片未全部上传完成");
        }
        mergeChunks(fileMd5, totalChunks);
        return Result.ok("合并成功", getFileUrl(fileMd5));
    }

    private void mergeChunks(String fileMd5, int totalChunks) throws IOException {
        File finalFile = new File(FINAL_DIR + fileMd5 + ".bin"); // 真实项目用原文件名
        try (FileOutputStream fos = new FileOutputStream(finalFile, true)) {
            for (int i = 0; i < totalChunks; i++) {
                File chunk = new File(TEMP_DIR + fileMd5 + "/" + fileMd5 + "_" + i);
                if (!chunk.exists()) throw new IOException("分片缺失:" + i);

                Files.copy(chunk.toPath(), fos);
                chunk.delete(); // 删除分片,节省空间
            }
        }
        // 存库、记录MD5、原文件名、大小、路径等
        saveToDb(fileMd5, finalFile);
        // 可选:删除临时文件夹
    }
}

四、关键优化点(生产必须做)

  1. 分片大小:5MB~20MB 最合适(太小请求多,太大重传代价高)
  2. 幂等性:同一个分片重复上传不报错
  3. 断点续传:前端记录已上传分片,后端返回已上传列表
  4. 秒传:文件MD5已存在直接返回成功
  5. 并发上传:前端用 Promise.all 或 Web Worker 并发 3~6 个分片
  6. 存储已上传分片:推荐用 Redis Set(file:{md5}:chunks),过期时间设长一点
  7. 文件名处理:前端传原始文件名,后端用 md5 + 后缀存储,数据库保存映射
  8. 进度条:前端计算已上传分片数 / 总分片数
  9. 失败重试:前端对失败分片自动重试 3 次
  10. Nginx 代理:上传走单独域名,配置 client_max_body_size 10G;

五、云厂商推荐(最省事)

  • 阿里云 OSS → 分片上传(Multipart Upload)
  • 腾讯云 COS → 分块上传
  • 七牛云 KODO → 分片上传
  • AWS S3 → Multipart Upload

后端基本只负责签名,前端直传,性能和稳定性最好。

六、总结一句话

普通小文件 → 用 MultipartFile 就行
真正的大文件(>500MB) → 必须做分片 + 断点续传 + 秒传,核心是“前端切片 + 后端记录分片状态 + 最后合并”
追求极致省心 → 直接用云存储的分片上传接口

你现在是想自己实现一套分片上传,还是接云厂商的?或者有具体问题(比如 Redis 怎么存、合并时怎么保证顺序等),可以继续说~

文章已创建 4455

发表回复

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

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部