一、前言:大文件上传的痛点与解决方案
在 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>***.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>***.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>***.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>***.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>***.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=true
username: root
password: root123456
driver-class-name: ***.mysql.cj.jdbc.Driver
# Redis配置
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 3000ms
# 上传文件临时存储路径
servlet:
multipart:
enabled: true
max-file-size: 10MB # 单个分片最大大小(需大于前端分片大小)
max-request-size: 100MB # 单次请求最大大小
# Spring Boot配置
server:
port: 8080
servlet:
context-path: /file-upload
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: ***.ken.file.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: isDeleted
logic-delete-value: 1
logic-not-delete-value: 0
# MinIO配置
minio:
endpoint: http://localhost:9000
a***ess-key: minioadmin
secret-key: minioadmin
bucket-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 ***MENT '主键ID' PRIMARY KEY,
file_md5 VARCHAR(32) NOT NULL ***MENT '文件唯一标识(MD5)',
file_name VARCHAR(255) NOT NULL ***MENT '原始文件名',
file_size BIGINT NOT NULL ***MENT '文件总大小(字节)',
chunk_total INT NOT NULL ***MENT '总分片数',
file_suffix VARCHAR(50) ***MENT '文件后缀(如mp4、zip)',
storage_type TINYINT NOT NULL DEFAULT 1 ***MENT '存储类型:1-本地存储,2-MinIO',
file_path VARCHAR(512) ***MENT '文件存储路径(本地路径或MinIO访问路径)',
status TINYINT NOT NULL DEFAULT 0 ***MENT '文件状态:0-上传中,1-上传完成,2-上传失败',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ***MENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ***MENT '更新时间',
is_deleted TINYINT NOT NULL DEFAULT 0 ***MENT '逻辑删除:0-未删除,1-已删除',
UNIQUE KEY uk_file_md5 (file_md5) ***MENT '文件MD5唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ***MENT='文件元数据表';
-- 分片信息表:存储单个分片的信息
CREATE TABLE IF NOT EXISTS file_chunk (
id BIGINT AUTO_INCREMENT ***MENT '主键ID' PRIMARY KEY,
file_md5 VARCHAR(32) NOT NULL ***MENT '文件唯一标识(关联file_metadata.file_md5)',
chunk_index INT NOT NULL ***MENT '分片索引(从0开始)',
chunk_size BIGINT NOT NULL ***MENT '分片大小(字节)',
chunk_path VARCHAR(512) NOT NULL ***MENT '分片存储路径',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ***MENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ***MENT '更新时间',
is_deleted TINYINT NOT NULL DEFAULT 0 ***MENT '逻辑删除:0-未删除,1-已删除',
UNIQUE KEY uk_file_md5_chunk_index (file_md5, chunk_index) ***MENT '文件MD5+分片索引唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ***MENT='分片信息表';
-- 索引优化:加快查询速度
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 ***.ken.file.entity;
import ***.baomidou.mybatisplus.annotation.IdType;
import ***.baomidou.mybatisplus.annotation.TableId;
import ***.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 ***.ken.file.entity;
import ***.baomidou.mybatisplus.annotation.IdType;
import ***.baomidou.mybatisplus.annotation.TableId;
import ***.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.format.annotation