在Web应用开发中,处理大文件上传一直是让开发者头疼的问题。当用户需要上传500MB的设计稿、2GB的视频素材或者10GB的数据库备份时,传统的表单直接上传方式会面临几个致命问题:
我去年为一个在线视频平台开发上传功能时,就遇到过用户上传4K视频频繁失败的情况。通过实现分块上传+断点续传的方案,最终将上传成功率从63%提升到了99.8%。下面分享具体实现方案。
前端需要将大文件切割为多个小块(通常1-5MB),每个块独立上传。关键实现点:
javascript复制// 使用File API获取文件对象
const file = document.getElementById('fileInput').files[0];
const chunkSize = 2 * 1024 * 1024; // 2MB分块
let offset = 0;
while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize);
uploadChunk(chunk, offset);
offset += chunkSize;
}
注意:分块大小需要权衡 - 太小会增加请求次数,太大会降低断点续传效果。实测2MB在大多数场景下表现最佳。
服务端需要三个核心接口:
Java示例(Spring Boot):
java复制@PostMapping("/initUpload")
public ResponseEntity<String> initUpload(@RequestParam String fileName) {
String uploadId = UUID.randomUUID().toString();
// 在Redis记录上传任务
redisTemplate.opsForHash().put(uploadId, "fileName", fileName);
return ResponseEntity.ok(uploadId);
}
关键是在服务端持久化上传进度。推荐方案:
java复制@PostMapping("/uploadChunk")
public ResponseEntity<?> uploadChunk(
@RequestParam String uploadId,
@RequestParam int chunkIndex,
@RequestParam MultipartFile chunk) {
// 检查是否已上传过该分块
if (redisTemplate.opsForSet().isMember(uploadId + ":chunks", chunkIndex)) {
return ResponseEntity.ok().build();
}
// 存储分块文件
chunk.transferTo(new File("/tmp/" + uploadId + "_" + chunkIndex));
// 记录上传进度
redisTemplate.opsForSet().add(uploadId + ":chunks", chunkIndex);
return ResponseEntity.ok().build();
}
javascript复制async function uploadFile(file) {
// 初始化上传
const { uploadId } = await fetch('/initUpload', {
method: 'POST',
body: JSON.stringify({ fileName: file.name }),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());
// 分块上传
for (let i = 0; i < Math.ceil(file.size / chunkSize); i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
await retryUpload(chunk, i, uploadId);
}
// 完成上传
await fetch('/completeUpload', {
method: 'POST',
body: JSON.stringify({ uploadId }),
headers: { 'Content-Type': 'application/json' }
});
}
所有分块上传完成后,按序号合并为完整文件:
java复制@PostMapping("/completeUpload")
public ResponseEntity<?> completeUpload(@RequestBody CompleteUploadRequest request) {
// 验证所有分块是否完整
Long chunkCount = redisTemplate.opsForSet().size(request.getUploadId() + ":chunks");
Long expectedCount = calculateExpectedChunks(request.getFileSize());
if (!chunkCount.equals(expectedCount)) {
return ResponseEntity.badRequest().body("Missing chunks");
}
// 合并文件
try (FileOutputStream fos = new FileOutputStream(finalFilePath)) {
for (int i = 0; i < expectedCount; i++) {
File chunkFile = new File("/tmp/" + request.getUploadId() + "_" + i);
Files.copy(chunkFile.toPath(), fos);
chunkFile.delete(); // 清理临时文件
}
}
// 清理Redis记录
redisTemplate.delete(request.getUploadId() + ":chunks");
return ResponseEntity.ok().build();
}
虽然可以并行上传多个分块,但需要注意:
建议方案:
javascript复制// 使用p-limit控制并发数
const limit = pLimit(3); // 最大3个并发
const uploadTasks = chunks.map((chunk, index) =>
limit(() => uploadChunk(chunk, index, uploadId))
);
await Promise.all(uploadTasks);
实际开发中遇到过的问题:
必须考虑的方面:
java复制// 文件类型验证示例
public boolean isSafeFile(MultipartFile file) {
String[] safeExtensions = {".jpg", ".png", ".mp4"};
String filename = file.getOriginalFilename().toLowerCase();
return Arrays.stream(safeExtensions).anyMatch(filename::endsWith);
}
在电商平台实施该方案时,我们收获了这些经验:
javascript复制// 本地保存上传进度
function saveProgress(uploadId, uploadedChunks) {
localStorage.setItem(uploadId, JSON.stringify(uploadedChunks));
}
// 恢复上传时读取进度
function getProgress(uploadId) {
return JSON.parse(localStorage.getItem(uploadId)) || [];
}
对于特别大的文件(如100GB以上),我们还实现了:
这套方案在日均上传量超过10TB的系统中稳定运行了2年多,期间根据实际需求不断优化,最终形成了这套可靠的大文件上传解决方案。