大文件上传解决方案
项目地址:https://github.com/webwss/Large-File-Upload-Template
在日常开发中,前端上传大文件是一个常见却易被忽略的性能瓶颈和用户体验问题
传统上传方式在面对100MB甚至GB级别的文件时,常会出现传输中断、上传失败等问题
因此,我引入了分片上传 + 断点续传 + 秒传机制,配合后端Spring Boot实现了一套高性能上传方案
项目技术选型
- 前端:Vue3 + Web Worker
- 后端:Spring Boot + MySQL
核心技术点
- 分片上传
- 断点续传
- 秒传
- Web Worker多线程加速计算
- 文件合并与临时文件清理
功能介绍
- 分片上传
将大文件拆分为多个小文件(chunk)上传,降低失败风险,提高稳定性
- 断点续传
支持中断后继续上传未完成的分片,避免重复上传
- 秒传
通过文件唯一标识(如MD5或Hash)判断服务器是否已存在该文件,若存在直接跳过上传,返回文件地址
上传流程
用户选择文件 ↓ 文件预处理(分片 + hash) ↓ checkFile → 是否已上传 → 是 → 秒传完成 ↓ 否 循环: checkChunk → 是否已上传 → 是 → 跳过 ↓ 否 uploadChunk(上传分片) ↓ 全部分片完成 ↓ mergeChunks(合并)存入数据库 ↓ 上传完成 ✅
前端实现关键点
文件分片&hash计算
我们使用Web Worker在子线程中计算fileHash,避免主线程阻塞
// 使用 crypto.subtle 进行 hash 计算
const hash = await crypto.subtle.digest("SHA-256", fileBuffer);
文件分片
const chunkSize = 1024 * 1024 * 2; // 2MB
const chunks = [];
for (let i = 0; i < file.size; i += chunkSize) {
chunks.push(file.slice(i, i + chunkSize));
}
上传参数格式
{
"chunk": Blob, // 分片内容
"index": "0", // 分片索引
"fileHash": "xxxxxxx" // 文件唯一标识
}
注意:chunk不能为空
后端实现关键点
分片上传
@Override
public String uploadChunk(MultipartFile chunk, String fileHash, String indexStr) {
int index = Integer.parseInt(indexStr);
File dir = new File(uploadPath + fileHash);
if (!dir.exists()) dir.mkdirs();
File chunkFile = new File(dir, String.valueOf(index));
try {
chunk.transferTo(chunkFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
return "chunk saved";
}
分片合并
/**
* 合并分片
* @param fileHash
* @param total
* @param fileName
* @return
*/
@Override
public Map<String, Object> merge(String fileHash, int total, String fileName) throws IOException {
File dir = new File(uploadPath + fileHash);
// 获取文件后缀名
String suffix = "";
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex != -1) {
suffix = fileName.substring(dotIndex); // 例如 ".mp4"
}
// 生成唯一文件名
String randomFileName = fileHash + suffix;
// 构造合并后的目标文件
File mergedFile = new File(uploadPath, randomFileName);
try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) {
for (int i = 0; i < total; i++) {
File chunkFile = new File(dir, String.valueOf(i));
if (!chunkFile.exists()) {
return Map.of("error", "Missing chunk: " + i);
}
try (FileInputStream fis = new FileInputStream(chunkFile)) {
StreamUtils.copy(fis, fos);
}
}
}
// 删除分片文件和目录
for (int i = 0; i < total; i++) {
new File(dir, String.valueOf(i)).delete();
}
dir.delete();
// 返回文件名给前端
Map<String, Object> response = new HashMap<>();
response.put("message", "merge complete");
response.put("fileName", randomFileName);
response.put("url", "/files/" + randomFileName); // 如果你支持静态访问的话
// 保存到数据库
UploadFile uploadFile = new UploadFile();
uploadFile.setFileHash(fileHash);
uploadFile.setFileUrl("/files/" + randomFileName);
save(uploadFile); // 保存到数据库
return response;
}
数据库
主要记录文件hash与对应位置,每次上传前查询是否存在,存在则跳过上传(秒传)