背景场景
在实际开发中,我们经常遇到大文件上传的需求,比如:
-
• 用户上传 2GB 的视频;
-
• 后台上传大文件 ZIP 数据包;
-
• 文件上传过程中网络中断。
如果我们仍用传统的 MultipartFile
一次性上传,很快会遇到以下问题:
-
• 前端体验差: 上传失败要重传;
-
• 后端压力大: 占用大量内存;
-
• 不具备扩展性: 难以支持分布式部署。
目标设计
设计一个支持以下特性的文件上传接口:
-
• 分片上传: 大文件拆成小块上传;
-
• 断点续传: 网络中断后从断点继续;
-
• 高可用性: 支持分布式部署,不依赖本地文件;
-
• 可扩展性: 支持对象存储如 MinIO、OSS;
-
• CDN 加速: 支持文件分发、下载优化。
技术选型
模块设计图
核心表结构设计
CREATE TABLE file_upload_record (
id BIGINTPRIMARY KEY AUTO_INCREMENT,
file_md5 VARCHAR(64) NOT NULL,
file_name VARCHAR(255),
total_chunks INT,
uploaded_chunks INTDEFAULT0,
is_complete BOOLEANDEFAULTFALSE,
created_at TIMESTAMPDEFAULTCURRENT_TIMESTAMP
);
核心接口设计
检查文件是否已上传
@GetMapping("/upload/check")
public ResponseEntity<?> checkFile(@RequestParam String fileMd5) {
FileUploadRecordrecord= recordRepository.findByFileMd5(fileMd5);
if (record != null && record.getIsComplete()) {
return ResponseEntity.ok(Map.of("uploaded", true, "url", getFileUrl(fileMd5)));
}
return ResponseEntity.ok(Map.of("uploaded", false));
}
上传分片接口
@PostMapping("/upload/chunk")
public ResponseEntity<?> uploadChunk(
@RequestParam String fileMd5,
@RequestParamint chunkIndex,
@RequestParam MultipartFile filePart
) throws IOException {
// 临时保存对象名:如 fileMd5/chunkIndex
StringobjectName= String.format("upload/%s/%d.part", fileMd5, chunkIndex);
// 上传到 MinIO
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(filePart.getInputStream(), filePart.getSize(), -1)
.contentType(filePart.getContentType())
.build()
);
// 更新数据库状态
recordService.markChunkUploaded(fileMd5, chunkIndex);
return ResponseEntity.ok("Chunk uploaded");
}
合并分片并生成最终文件
@PostMapping("/upload/merge")
public ResponseEntity<?> mergeChunks(@RequestParam String fileMd5, @RequestParamint totalChunks) throws Exception {
StringfinalObjectName="upload/" + fileMd5 + ".final";
// 合并文件(使用 MinIO 的 composeObject)
List<ComposeSource> sources = newArrayList<>();
for (inti=0; i < totalChunks; i++) {
sources.add(
ComposeSource.builder()
.bucket(bucketName)
.object(String.format("upload/%s/%d.part", fileMd5, i))
.build()
);
}
minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket(bucketName)
.object(finalObjectName)
.sources(sources)
.build()
);
// 标记上传完成
recordService.markComplete(fileMd5);
return ResponseEntity.ok(Map.of("url", getFileUrl(fileMd5)));
}
核心逻辑详解
为什么要用 fileMd5 作为分片唯一标识?
-
• MD5 可标志文件唯一性,便于去重和断点续传;
-
• 客户端可预先计算 MD5,与服务器确认哪些分片已上传。
MinIO 是如何支持分片合并的?
-
• MinIO 支持
composeObject
,可将多个对象合并为一个; -
• 类似于 AWS S3 的“
多段上传
”功能; -
• 合并操作在服务端进行,避免网络传输压力。
如果网络中断怎么办?
-
• 客户端每上传一个分片记录状态;
-
• 重新发起上传前调用
/check
查询已上传分片; -
• 只上传缺失部分,节省时间和带宽。
可扩展性设计
模块化存储接口
publicinterfaceFileStorageService {
voiduploadChunk(String objectName, InputStream stream, long size);
voidmergeChunks(String finalName, List<String> chunkNames);
String getFileUrl(String objectName);
}
-
• 实现类可对接 OSS、MinIO、FastDFS 等;
-
• 降低耦合,支持替换存储供应商。
性能与高可用优化
返回 CDN 地址
public String getFileUrl(String fileMd5) {
return cdnDomain + "/upload/" + fileMd5 + ".final";
}
-
• 文件合并后部署到 CDN;
-
• 前端访问地址统一为 CDN 加速域名。
总结
我们通过以下方案构建了一个高可用、可扩展的大文件上传接口:
-
• 支持 断点续传、分片上传;
-
• 使用 MinIO/S3 对象存储;
-
• 支持 CDN 分发;
-
• 可轻松扩展存储后端或接入云服务。
推荐工具 & 框架
-
• MinIO Java SDK
-
• Spring Boot + Spring Web
-
• Redis(限流、幂等)
-
• MySQL(记录上传状态)
结语
这套文件上传方案,特别适合对稳定性
、高并发
、用户体验
要求较高的系统。希望本文能为你在实际项目中实现大文件上传提供思路。