一、为什么大文件上传是个技术活
想象一下你要把一部4K电影上传到网盘,或者把几个GB的日志文件传给同事。如果直接扔进普通表单,浏览器可能会卡死,服务器也可能崩溃。这就像用自行车运冰箱——不是不行,但肯定不是最优解。
大文件上传的核心痛点有三个:
- 稳定性:网络抖动或中断会导致前功尽弃
- 性能:内存溢出风险高,尤其Java应用默认堆内存有限
- 体验:用户需要看到进度条,而不是"假死"的页面
二、分片上传:把大象切成牛排
分片上传是当前最主流的解决方案,原理就像用快递寄家具——拆成零件分批运输,到货后再组装。
技术栈选择:Spring Boot + 阿里云OSS SDK(其他云服务类似)
// 文件分片工具类示例
public class FileSplitter {
/**
* 将文件切分为指定大小的分片
* @param sourceFile 原始文件
* @param chunkSize 分片大小(单位:MB)
* @return 分片临时文件列表
*/
public static List<File> split(File sourceFile, int chunkSize) throws IOException {
List<File> chunks = new ArrayList<>();
try (InputStream in = new FileInputStream(sourceFile)) {
byte[] buffer = new byte[chunkSize * 1024 * 1024];
int bytesRead;
int index = 0;
while ((bytesRead = in.read(buffer)) > 0) {
File chunkFile = new File(sourceFile.getParent(),
sourceFile.getName() + ".part" + index);
try (OutputStream out = new FileOutputStream(chunkFile)) {
out.write(buffer, 0, bytesRead);
}
chunks.add(chunkFile);
index++;
}
}
return chunks;
}
}
关键参数建议:
- 分片大小通常设为5-10MB(平衡网络开销和并发效率)
- 需要记录分片序号用于服务端重组
三、断点续传:让上传像游戏存档
分片只是基础,真正的可靠性要靠断点续传实现。这里需要解决两个问题:
- 客户端记录:本地存储已上传分片信息
- 服务端校验:避免重复上传
前端实现示例(Vue + axios):
// 上传状态管理类
class UploadTracker {
constructor(fileId) {
this.fileId = fileId;
this.chunks = JSON.parse(localStorage.getItem(`upload_${fileId}`)) || [];
}
// 记录成功上传的分片
markChunkComplete(chunkIndex) {
if (!this.chunks.includes(chunkIndex)) {
this.chunks.push(chunkIndex);
localStorage.setItem(`upload_${fileId}`, JSON.stringify(this.chunks));
}
}
// 获取待上传分片
getPendingChunks(totalChunks) {
return Array.from({length: totalChunks}, (_, i) => i)
.filter(i => !this.chunks.includes(i));
}
}
服务端校验接口(Spring MVC):
@RestController
@RequestMapping("/upload")
public class UploadController {
@GetMapping("/check")
public ResponseEntity<Map<String, Object>> checkChunk(
@RequestParam String fileMd5,
@RequestParam int chunkIndex) {
// 实际项目中应查询数据库或分布式缓存
boolean exists = checkChunkExistsInStorage(fileMd5, chunkIndex);
Map<String, Object> response = new HashMap<>();
response.put("exists", exists);
response.put("chunkIndex", chunkIndex);
return ResponseEntity.ok(response);
}
private boolean checkChunkExistsInStorage(String fileMd5, int chunkIndex) {
// 伪代码:检查OSS等存储服务
return OSSClient.doesObjectExist(bucketName,
"temp/" + fileMd5 + "/" + chunkIndex);
}
}
四、实战中的进阶技巧
4.1 秒传优化
通过文件指纹(MD5/SHA1)实现"文件去重":
// 使用Apache Commons Codec计算文件指纹
public static String calculateFileMd5(File file) throws IOException {
try (InputStream is = new FileInputStream(file)) {
return DigestUtils.md5Hex(is);
}
}
4.2 并发控制
浏览器并发请求数有限制,建议采用令牌桶算法控制:
// 简易令牌桶实现
public class UploadThrottler {
private final Semaphore semaphore;
public UploadThrottler(int maxConcurrent) {
this.semaphore = new Semaphore(maxConcurrent);
}
public boolean tryAcquire() {
return semaphore.tryAcquire();
}
public void release() {
semaphore.release();
}
}
4.3 服务端合并策略
分片全部上传后,建议用异步任务合并:
// 使用Spring异步任务合并文件
@Service
public class FileMergeService {
@Async
public void mergeFiles(String fileMd5, String fileName) {
List<OSSObjectSummary> chunks = listChunksFromOSS(fileMd5);
// 按分片序号排序后合并
chunks.sort(Comparator.comparingInt(this::extractChunkIndex));
try (OutputStream mergedStream = new FileOutputStream(finalFile)) {
for (OSSObjectSummary chunk : chunks) {
OSSObject object = ossClient.getObject(bucketName, chunk.getKey());
try (InputStream chunkStream = object.getObjectContent()) {
IOUtils.copy(chunkStream, mergedStream);
}
}
}
// 清理临时分片
deleteChunks(chunks);
}
}
五、避坑指南
- 内存泄漏:务必关闭所有InputStream/OutputStream
- 文件名冲突:使用UUID代替原始文件名存储
- 权限控制:临时分片目录需要设置过期时间
- 监控报警:对失败分片建立重试机制
六、技术选型对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 普通表单上传 | 实现简单 | 超过100MB风险极高 |
| 分片上传 | 支持断点续传 | 需要前后端协同开发 |
| WebSocket | 实时性高 | 服务端资源消耗大 |
| 第三方SDK | 快速集成 | 可能有vendor lock-in |
七、总结
大文件上传就像搬家——用对工具才能省时省力。分片上传是基础,断点续传是保障,而良好的用户体验才是终极目标。在实际项目中,建议:
- 优先使用成熟的对象存储服务(OSS/S3)
- 前端做好错误处理和进度展示
- 服务端实现幂等性接口
记住:没有完美的方案,只有最适合业务场景的取舍。
评论