如何设计一个高可用、可扩展的文件上传接口?
作者:一位八年经验的 Java 开发工程师
关键词:分片上传、断点续传、CDN、对象存储、MinIO、大文件、网络不稳定、Spring Boot
✨ 背景场景
在实际开发中,我们经常遇到大文件上传的需求,比如:
- 用户上传 2GB 的视频;
- 后台上传大文件 ZIP 数据包;
- 文件上传过程中网络中断。
如果我们仍用传统的 MultipartFile
一次性上传,很快会遇到以下问题:
- 前端体验差:上传失败要重传;
- 后端压力大:占用大量内存;
- 不具备扩展性:难以支持分布式部署。
🎯 目标设计
设计一个支持以下特性的文件上传接口:
- ✅ 分片上传:大文件拆成小块上传;
- ✅ 断点续传:网络中断后从断点继续;
- ✅ 高可用性:支持分布式部署,不依赖本地文件;
- ✅ 可扩展性:支持对象存储如 MinIO、OSS;
- ✅ CDN 加速:支持文件分发、下载优化。
🧠 技术选型
技术项 | 说明 |
---|---|
Spring Boot | 主体框架 |
MinIO | 对象存储(可兼容 OSS) |
MySQL | 存储文件上传状态、分片记录 |
Redis | 控制并发、分片状态缓存(可选) |
CDN | 文件分发加速(如七牛、阿里云) |
🧩 模块设计图
前端上传分片 ------> 接口接收分片 ------> 存储到对象存储 ------> 标记上传状态 ------> 所有分片上传完合并 ------> 返回访问地址
📦 核心表结构设计
sql
CREATE TABLE file_upload_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
file_md5 VARCHAR(64) NOT NULL,
file_name VARCHAR(255),
total_chunks INT,
uploaded_chunks INT DEFAULT 0,
is_complete BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
🔧 核心接口设计
1️⃣ 检查文件是否已上传
less
@GetMapping("/upload/check")
public ResponseEntity<?> checkFile(@RequestParam String fileMd5) {
FileUploadRecord record = 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));
}
2️⃣ 上传分片接口
less
@PostMapping("/upload/chunk")
public ResponseEntity<?> uploadChunk(
@RequestParam String fileMd5,
@RequestParam int chunkIndex,
@RequestParam MultipartFile filePart
) throws IOException {
// 临时保存对象名:如 fileMd5/chunkIndex
String objectName = 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");
}
3️⃣ 合并分片并生成最终文件
less
@PostMapping("/upload/merge")
public ResponseEntity<?> mergeChunks(@RequestParam String fileMd5, @RequestParam int totalChunks) throws Exception {
String finalObjectName = "upload/" + fileMd5 + ".final";
// 合并文件(使用 MinIO 的 composeObject)
List<ComposeSource> sources = new ArrayList<>();
for (int i = 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
查询已上传分片; - 只上传缺失部分,节省时间和带宽。
🚀 可扩展性设计
🧱 模块化存储接口
arduino
public interface FileStorageService {
void uploadChunk(String objectName, InputStream stream, long size);
void mergeChunks(String finalName, List<String> chunkNames);
String getFileUrl(String objectName);
}
- 实现类可对接 OSS、MinIO、FastDFS 等;
- 降低耦合,支持替换存储供应商。
📈 性能与高可用优化
优化点 | 技术方案 |
---|---|
上传限速 | Nginx 限流、前端分批上传 |
接口幂等 | Redis 控制 chunk 上传状态 |
合并耗时 | 可异步合并 + 任务中心 |
多节点上传 | 存储层采用对象存储,支持多节点共享 |
📦 返回 CDN 地址
typescript
public String getFileUrl(String fileMd5) {
return cdnDomain + "/upload/" + fileMd5 + ".final";
}
- 文件合并后部署到 CDN;
- 前端访问地址统一为 CDN 加速域名。
✅ 总结
我们通过以下方案构建了一个高可用、可扩展的大文件上传接口:
- 支持 断点续传 、分片上传;
- 使用 MinIO/S3 对象存储;
- 支持 CDN 分发;
- 可轻松扩展存储后端或接入云服务。
🛠️ 推荐工具 & 框架
- MinIO Java SDK
- Spring Boot + Spring Web
- Redis(限流、幂等)
- MySQL(记录上传状态)
🙋♂️ 结语
这套文件上传方案,特别适合对稳定性、高并发、用户体验要求较高的系统。希望本文能为你在实际项目中实现大文件上传提供思路。