公司的用户上传 2GB 的视频文件到网盘,传了 80% 的时候网络断了。页面刷新后,上传列表里空空如也——前端状态丢失,后端也没记录哪些分片已经上传成功。用户只能从头再来。2GB 的文件,从头再传一遍,用户的耐心也从头归零。
分片上传天然支持断点续传——前提是你知道哪些分片已经上传成功了。不知道的话,就只能全量重传。今天聊聊怎么用 Redis 记录每个分片的上传状态,让网络断了之后从断点秒级续传,而不是从头开始。
分片上传的断点问题
分片上传的流程很简单:前端把大文件切成 N 片,逐片上传。后端每收到一片就存起来。
文件 2GB → 切 200 片 × 10MB → 逐片上传
如果第 150 片上传失败(网络断了、服务器卡了),最好的情况是从第 150 片继续传。但前端不知道自己传了哪些片——因为前端的进度存在内存里,页面一刷新就没了。
后端需要提供一个能力:告诉前端哪些分片已经上传成功了。
Redis 记录分片状态
给每个上传任务一个 uploadId(UUID),用 Redis 的 Set 记录已上传的分片编号:
// 记录分片已上传
redis.sadd("upload:" + uploadId + ":parts", String.valueOf(partNo));
// 查询已上传的分片列表
Set<String> completed = redis.smembers("upload:" + uploadId + ":parts");
// 全部上传完 → 合并
long totalParts = redis.get("upload:" + uploadId + ":total");
if (completed.size() == totalParts) {
mergeFile(uploadId);
}
Redis Set 的 sadd 天然幂等——同一片重复上传不会报错。smembers 返回所有已上传分片,前端一查就知道"0~149 已经传完了,从第 150 片开始"。
断点续传的完整流程
前端:启动上传
├─ 计算文件 MD5 + 总分片数
├─ POST /api/upload/init → 返回 uploadId
│ 后端:生成 uploadId,记录总分片数到 Redis
│
├─ 逐片上传(POST /api/upload/part)
│ 后端:sadd upload:{id}:parts {partNo},写文件
│
├─ 网络中断 → 页面刷新
│
├─ 重新启动上传(同一文件同一 MD5)
│ 前端:POST /api/upload/resume?md5=xxx
│ 后端:根据 MD5 找到之前的 uploadId
│ 返回:{ uploadId, completed: [0,1,2,...149] }
│
├─ 从第 150 片继续传
│
└─ 全部完成 → POST /api/upload/merge
后端:校验完整性 → 合并分片 → 清理 Redis
用文件 MD5 做断点续传的 key。同一个文件,即使两次上传间隔了几天,只要 MD5 一样,就能找到之前的进度继续传。
Redis 过期时间:别让 Key 永久占用
分片状态不用永久保留。上传完成后立即删,如果上传中断了,Key 设一个合理过期时间(比如 24 小时),过期自动清理:
// 初始化时设过期
redis.set("upload:" + uploadId + ":total", String.valueOf(totalParts), 24 * 3600);
// 每上传一片,续期一次
redis.expire("upload:" + uploadId + ":parts", 24 * 3600);
24 小时是合理的——用户如果 24 小时都没重新上传,大概率放弃了,清理状态释放 Redis 内存。
前端怎么判定"同一个文件"
不是靠文件名,是靠文件 MD5。文件名改了、路径改了、甚至文件在本地被移动了——只要内容没变,MD5 一样,就能续传。
// 前端计算文件 MD5(Web Crypto API)
async function fileMd5(file) {
const buffer = await file.arrayBuffer();
const hash = await crypto.subtle.digest('SHA-256', buffer);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0')).join('');
}
计算 MD5 本身也要时间——2GB 的文件,浏览器里算 MD5 要好几秒。可以异步在 Worker 线程里算,不阻塞 UI。
完整的后端实现
@PostMapping("/upload/init")
public UploadInfo init(@RequestParam String md5, @RequestParam int totalParts) {
// 先查是否存在相同 MD5 的上传任务
String existingId = redis.get("upload:md5:" + md5);
if (existingId != null) {
Set<String> parts = redis.smembers("upload:" + existingId + ":parts");
return new UploadInfo(existingId, parts); // 返回已完成的分片
}
String uploadId = UUID.randomUUID().toString();
redis.set("upload:md5:" + md5, uploadId, 24 * 3600);
redis.set("upload:" + uploadId + ":total", String.valueOf(totalParts), 24 * 3600);
return new UploadInfo(uploadId, Collections.emptySet());
}
@PostMapping("/upload/part")
public void uploadPart(@RequestParam String uploadId,
@RequestParam int partNo, @RequestPart MultipartFile part) {
savePartFile(uploadId, partNo, part);
redis.sadd("upload:" + uploadId + ":parts", String.valueOf(partNo));
redis.expire("upload:" + uploadId + ":parts", 24 * 3600);
}
总结
分片上传的断点续传,核心就是"记住哪些片已经传过了"。
- Redis Set 记录分片——
sadd幂等、smembers查询 - 文件 MD5 做断点 key——同一文件多次上传自动续传
- 24 小时过期自动清理——不上传了不占 Redis 内存
配完之后,2GB 文件传了 80% 断了,刷新页面秒级续传剩下的 20%,用户体验从"崩溃"到"无感"。
