Java 大文件上传实战:从底层原理到分布式落地(含分片 / 断点续传 / 秒传)
一、前言:大文件上传的痛点与解决方案
在 Java 开发中,大文件上传是高频需求也是技术难点。传统单文件直接上传方案在面对 1GB 以上文件时,常会出现超时失败、内存溢出、用户体验差等问题 —— 比如网络波动导致上传中断后需重新上传、大文件加载占用过多内存导致服务 OOM、上传进度无法感知等。
本文将从底层原理出发,结合实战场景,手把手实现一套支持分片上传、断点续传、秒传、并发控制的大文件上传方案。方案基于 JDK 17+Spring Boot 3.x 构建,整合 MyBatis-Plus、MinIO、Redis 等组件,所有代码均可直接运行,同时兼顾深度与可读性,让新手能快速上手,资深开发者能夯实底层逻辑。
二、底层原理:看透大文件上传的核心逻辑
2.1 为什么传统单文件上传行不通?
传统上传方案是将文件作为一个整体通过 HTTP 请求发送到服务端,核心问题集中在三点:
- 超时风险:大文件传输时间长,容易触发 HTTP 超时(默认 Tomcat 超时为 60 秒),网络波动时直接上传失败;
- 内存压力:服务端接收文件时,会将整个文件加载到内存处理,1GB 文件可能直接导致 JVM OOM;
- 体验极差:上传中断后需重新上传整个文件,无进度反馈,用户无法预估时间。
2.2 核心解决方案:分片上传
2.2.1 分片上传原理
分片上传是将大文件拆分为多个小分片(如 5MB / 片),分别上传到服务端,服务端接收完所有分片后再合并为原始文件。核心流程如下:

2.2.2 关键技术点拆解
- 文件唯一标识:用文件 MD5 作为唯一标识,确保分片与原始文件一一对应,也是秒传和断点续传的核心依据;
- 分片拆分规则:按固定大小拆分(如 5MB),最后一片大小可能小于固定值,需记录总分片数和当前分片索引;
- 分片传输保障:每个分片上传时携带 MD5、分片索引、总分片数等元数据,服务端校验合法性;
- 合并逻辑:所有分片上传完成后,按分片索引顺序合并,避免文件损坏。
2.3 断点续传与秒传的底层逻辑
2.3.1 断点续传原理
断点续传基于分片上传,核心是「记录已上传分片」,避免重复上传:
- 前端上传前先查询服务端:该文件已上传的分片索引列表;
- 前端跳过已上传分片,仅上传未完成的分片;
- 支持暂停 / 继续功能:暂停时仅停止分片上传,不清理已上传分片;继续时重复第一步逻辑。

2.3.2 秒传原理
秒传的核心是「文件预校验」,本质是利用文件唯一标识(MD5)快速判断文件是否已存在:
- 前端计算文件 MD5 后,先向服务端发送秒传校验请求;
- 服务端查询该 MD5 对应的文件是否已上传完成;
- 若已存在,则直接返回秒传成功,无需上传任何分片;
- 若不存在或未上传完成,则进入分片上传流程。
2.3.3 易混淆点区分
| 技术点 | 核心逻辑 | 适用场景 |
|---|---|---|
| 分片上传 | 拆分文件 + 分块传输 + 合并 | 所有大文件上传(100MB+) |
| 断点续传 | 记录已上传分片 + 跳过重复上传 | 网络不稳定、大文件长时间上传 |
| 秒传 | MD5 预校验 + 已上传文件匹配 | 重复上传同一文件(如用户二次上传) |
2.4 关键技术选型说明
| 组件 | 版本号 | 作用说明 | 选择理由 |
|---|---|---|---|
| JDK | 17 | 开发运行环境 | 长期支持版本,兼容 Spring Boot 3.x |
| Spring Boot | 3.2.5 | 项目基础框架 | 最新稳定版,支持 JDK 17+ |
| MyBatis-Plus | 3.5.4.4 | 持久层框架 | 简化 CRUD,支持分页、乐观锁等 |
| MinIO | 8.5.12 | 分布式文件存储 | 轻量、兼容 S3 协议,适合大文件存储 |
| Redis | 7.2.4 | 分布式锁、缓存已上传分片 | 高性能,支持分布式场景 |
| Fastjson2 | 2.0.49 | JSON 序列化 / 反序列化 | 速度快,支持 JDK 17+ |
| Lombok | 1.18.30 | 简化 POJO 代码 | 减少 getter/setter 等冗余代码 |
| Springdoc-OpenAPI | 2.2.0 | Swagger3 接口文档 | 适配 Spring Boot 3.x,替代 Springfox |
| Vue 3 | 3.4.21 | 前端框架 | 轻量、响应式,适合上传组件开发 |
| Axios | 1.6.8 | 前端 HTTP 请求工具 | 支持中断请求、进度监听 |
| Spark-MD5 | 3.0.2 | 前端大文件 MD5 计算 | 支持分片计算 MD5,避免内存溢出 |
三、实战准备:环境搭建与项目初始化
3.1 开发环境要求
- JDK 17(需配置环境变量)
- MySQL 8.0(用于存储文件元数据、分片信息)
- Redis 7.0+(用于分布式锁、缓存)
- MinIO 8.5+(分布式存储,可选本地存储替代)
- Maven 3.8+(项目构建工具)
- Node.js 16+(前端项目运行)
3.2 后端项目初始化(Spring Boot)
3.2.1 pom.xml 依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.5</version><relativePath/></parent><groupId>com.ken.file</groupId><artifactId>large-file-upload</artifactId><version>0.0.1-SNAPSHOT</version><name>large-file-upload</name><description>Java大文件上传实战项目</description><properties><java.version>17</java.version><mybatis-plus.version>3.5.4.4</mybatis-plus.version><minio.version>8.5.12</minio.version><fastjson2.version>2.0.49</fastjson2.version><springdoc.version>2.2.0</springdoc.version><guava.version>32.1.3-jre</guava.version></properties><dependencies><!-- Spring Boot核心依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- MyBatis-Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><!-- 数据库驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- MinIO --><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>${minio.version}</version></dependency><!-- JSON处理 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>${fastjson2.version}</version></dependency><!-- 工具类 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>${guava.version}</version></dependency><!-- Swagger3 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>${springdoc.version}</version></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
3.2.2 核心配置文件(application.yml)
spring:# 数据源配置datasource:url: jdbc:mysql://localhost:3306/large_file_upload?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: root123456driver-class-name: com.mysql.cj.jdbc.Driver# Redis配置redis:host: localhostport: 6379password:database: 0timeout: 3000ms# 上传文件临时存储路径servlet:multipart:enabled: truemax-file-size: 10MB # 单个分片最大大小(需大于前端分片大小)max-request-size: 100MB # 单次请求最大大小# Spring Boot配置
server:port: 8080servlet:context-path: /file-upload# MyBatis-Plus配置
mybatis-plus:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.ken.file.entityconfiguration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type: autologic-delete-field: isDeletedlogic-delete-value: 1logic-not-delete-value: 0# MinIO配置
minio:endpoint: http://localhost:9000access-key: minioadminsecret-key: minioadminbucket-name: large-file-bucket # 存储大文件的桶名(需提前创建)# 自定义上传配置
file:upload:chunk-size: 5242880 # 分片大小5MB(5*1024*1024)local-storage-path: D:/large-file-upload/local-storage # 本地存储路径(单机模式)expire-days: 7 # 未完成上传的分片过期时间(天)
3.3 数据库设计(MySQL 8.0)
需创建两张核心表:file_metadata(文件元数据表)和file_chunk(分片信息表),SQL 脚本如下:
CREATE DATABASE IF NOT EXISTS large_file_upload DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE large_file_upload;-- 文件元数据表:存储文件整体信息
CREATE TABLE IF NOT EXISTS file_metadata (id BIGINT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY,file_md5 VARCHAR(32) NOT NULL COMMENT '文件唯一标识(MD5)',file_name VARCHAR(255) NOT NULL COMMENT '原始文件名',file_size BIGINT NOT NULL COMMENT '文件总大小(字节)',chunk_total INT NOT NULL COMMENT '总分片数',file_suffix VARCHAR(50) COMMENT '文件后缀(如mp4、zip)',storage_type TINYINT NOT NULL DEFAULT 1 COMMENT '存储类型:1-本地存储,2-MinIO',file_path VARCHAR(512) COMMENT '文件存储路径(本地路径或MinIO访问路径)',status TINYINT NOT NULL DEFAULT 0 COMMENT '文件状态:0-上传中,1-上传完成,2-上传失败',create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',UNIQUE KEY uk_file_md5 (file_md5) COMMENT '文件MD5唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件元数据表';-- 分片信息表:存储单个分片的信息
CREATE TABLE IF NOT EXISTS file_chunk (id BIGINT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY,file_md5 VARCHAR(32) NOT NULL COMMENT '文件唯一标识(关联file_metadata.file_md5)',chunk_index INT NOT NULL COMMENT '分片索引(从0开始)',chunk_size BIGINT NOT NULL COMMENT '分片大小(字节)',chunk_path VARCHAR(512) NOT NULL COMMENT '分片存储路径',create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',UNIQUE KEY uk_file_md5_chunk_index (file_md5, chunk_index) COMMENT '文件MD5+分片索引唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分片信息表';-- 索引优化:加快查询速度
CREATE INDEX idx_file_md5_status ON file_metadata(file_md5, status);
CREATE INDEX idx_file_md5 ON file_chunk(file_md5);
3.4 前端项目初始化(Vue 3)
3.4.1 项目创建与依赖安装
# 创建Vue项目
npm create vue@latest large-file-upload-frontend
cd large-file-upload-frontend
# 安装核心依赖
npm install axios@1.6.8 spark-md5@3.0.2 element-plus@2.7.0
3.4.2 前端核心配置(src/utils/request.js)
import axios from 'axios';// 创建Axios实例
const service = axios.create({baseURL: 'http://localhost:8080/file-upload',timeout: 60000, // 分片上传超时时间(1分钟)headers: {'Content-Type': 'application/json'}
});// 请求拦截器
service.interceptors.request.use(config => {// 可添加token等认证信息return config;},error => {Promise.reject(error);}
);// 响应拦截器
service.interceptors.response.use(response => {const res = response.data;if (res.code !== 200) {console.error('请求失败:', res.msg);return Promise.reject(res);}return res;},error => {console.error('请求异常:', error.message);return Promise.reject(error);}
);export default service;
四、核心组件开发:后端实现
4.1 实体类设计(Entity)
4.1.1 文件元数据实体(FileMetadata.java)
package com.ken.file.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;/*** 文件元数据表实体* @author ken*/
@Data
@TableName("file_metadata")
public class FileMetadata {/*** 主键ID*/@TableId(type = IdType.AUTO)private Long id;/*** 文件唯一标识(MD5)*/private String fileMd5;/*** 原始文件名*/private String fileName;/*** 文件总大小(字节)*/private Long fileSize;/*** 总分片数*/private Integer chunkTotal;/*** 文件后缀(如mp4、zip)*/private String fileSuffix;/*** 存储类型:1-本地存储,2-MinIO*/private Integer storageType;/*** 文件存储路径(本地路径或MinIO访问路径)*/private String filePath;/*** 文件状态:0-上传中,1-上传完成,2-上传失败*/private Integer status;/*** 创建时间*/@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createTime;/*** 更新时间*/@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime updateTime;/*** 逻辑删除:0-未删除,1-已删除*/private Integer isDeleted;
}
4.1.2 分片信息实体(FileChunk.java)
package com.ken.file.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.format.annotation