为什么要做切片上传
普通文件上传在小图片、小附件场景里没什么问题,但一旦换成视频,问题就会集中暴露:文件体积大、上传时间长、网络中断概率高,失败后如果只能从头再传,用户体验会非常差。
这个项目里的视频上传最终拆成了两段:
- 文件服务负责原始文件上传:分片检查、分片落盘、合并、上传 MinIO、保存上传记录。
- 社区服务负责视频业务状态:保存草稿、提交审核、转码回调、审核发布。
这样拆开后,上传链路只关心“文件能不能可靠拿到一个正式地址”,视频业务链路再基于这个地址继续处理标题、封面、标签、审核和转码。
整体设计
切片上传的核心目标不是把文件切开这么简单,而是让上传过程具备三个能力:
- 秒传:同一个 MD5 的文件已经上传过时,直接复用历史文件地址。
- 断点续传:只上传服务端缺失的分片,避免中断后从头开始。
- 可清理:用户取消上传或合并成功后,及时删除本地分片临时目录。
当前实现里,前端需要先计算完整文件 MD5,并按固定分片大小得到 total。服务端用 md5 作为一次完整上传任务的唯一标识,所有分片都会落到同一个临时目录下:
draft.file.chunk-dir/{md5}/{index}.part
合并成功后,正式文件会上传到 MinIO,并保存到 file_upload_record 表。这个表后续有两个作用:上传检查时用于秒传,视频转码时用于根据原始视频地址反查对象名称和本地源文件缓存。
上传流程
前端实际调用接口时,大概是这个顺序:
GET /draft-file/file/upload-chunk/check?md5=...&total=...POST /draft-file/file/upload-chunk上传缺失分片,表单字段是chunk、md5、index、total。POST /draft-file/file/merge-chunks合并分片,参数是md5、total、suffix。- 如果用户取消上传,调用
DELETE /draft-file/file/delete?md5=...删除本地临时分片。
这里的网关路径会转发到文件服务内的 /file/... 接口。对于前端来说,只需要按网关路径调用即可。
服务端如何保存和合并分片
分片上传接口本身不做复杂逻辑,它只负责保存当前分片:
{chunk-dir}/{md5}/{index}.part
如果同一个 index 重复上传,后上传的分片会覆盖旧文件。这个特性反而方便前端做失败重试:某个分片传坏了,重新传同一个索引即可。
合并接口才是整个上传链路的关键。它会先扫描分片目录,确认 0 到 total - 1 的分片都存在。如果缺少分片,就返回 missingIndexes,让前端补传后再合并。
合并成功后的服务端处理顺序是:
- 按索引顺序读取所有
.part文件。 - 生成一个完整临时文件。
- 上传完整文件到 MinIO。
- 把完整源文件转存到
draft.file.source-dir。 - 写入或更新
file_upload_record。 - 写入 Redis 秒传缓存,默认缓存 7 天。
- 删除本地分片目录。
MinIO 对象路径当前按日期和 MD5 生成:
file/yyyyMMdd/{md5}.{suffix}
这个命名方式的好处是同一个文件天然稳定,后续通过 MD5 查到的记录也能直接复用。
秒传、断点续传和取消上传
秒传
上传前的检查接口会先查 Redis:
file:upload:md5:{md5}
Redis 没命中时再查 file_upload_record。只要找到 status = 1 的完成记录,就说明这个文件已经上传过,服务端会直接返回历史文件地址,前端可以跳过分片上传和合并。
断点续传
如果没有完成记录,服务端会扫描本地分片目录,把已经存在的 .part 文件下标返回给前端。前端拿到 uploadedIndexes 后,只上传缺失分片。
比如总共有 5 个分片,服务端返回:
[0, 1, 3]
那前端只需要继续上传 2 和 4。上传完成后再调用合并接口。
取消上传
用户取消上传时,前端只要把 md5 传给删除接口。后端会删除:
draft.file.chunk-dir/{md5}
删除后这个文件的本地分片状态就清空了。如果用户之后重新上传,需要重新提交所有分片,除非已经有完成记录能命中秒传。
上传后的视频发布与转码
文件上传完成后,前端拿到的是一个原始视频地址。它还不是最终用户播放时的多清晰度视频,而是视频业务的输入。
接下来的业务链路在社区服务里完成:
- 前端调用
/draft-community/video/draft保存视频草稿,把上传结果里的url填到videoUrl。 - 用户点击提交审核后,调用
/draft-community/video/{id}/submit。 - 社区服务校验视频归属和状态,把视频状态改为
TRANSCODING。 - 社区服务通过 Feign 调用文件服务提交转码任务。
- 文件服务异步执行 FFmpeg 转码,生成 360P、480P、720P、1080P 和缩略图。
- 文件服务把转码结果回调给社区服务。
- 社区服务写入多清晰度地址,转码成功进入
REVIEWING,失败进入TRANSCODE_FAILED。
完整时序如下:
视频状态流转也比较清晰:
转码时为什么要保留完整源文件
分片合并后,服务端不仅会把文件上传到 MinIO,还会把完整源文件保存到本地 source-dir。这个设计是为了转码复用。
视频提交审核后,转码服务会优先根据 videoUrl 查询 file_upload_record,拿到 MinIO 的对象名称。然后它会先找本地源文件缓存,如果存在就直接使用;本地没有,再从 MinIO 下载;如果连对象名称都解析不到,最后才按原始 URL 下载。
这样做有两个好处:
- 同一个源文件重复转码时,不需要每次都从 MinIO 下载。
- 本地源文件目录和 MinIO 对象路径保持一致,排查问题时更容易定位。
完整源文件不会永久保存。项目里有定时清理任务,每 3 天扫描一次 source-dir,删除超过 3 天没有被使用的源文件,并清理空目录。
踩坑与注意点
MD5 必须是完整文件的 MD5
秒传、断点续传、分片目录隔离、上传记录复用都依赖这个 MD5。如果前端传的是分片 MD5,整个链路都会失效。
分片索引从 0 开始
当前合并逻辑会从 0 遍历到 total - 1。前端如果从 1 开始编号,服务端会认为 0.part 缺失,合并必然失败。
分片大小不能超过后端 multipart 限制
draft-file 当前开发配置里单个文件和请求大小限制是 70MB。前端分片大小要小于这个限制,否则单片上传就会失败。
多实例部署要注意分片目录
当前分片先落本地目录。如果文件服务部署多个实例,第一次上传的分片和后续合并请求可能落到不同机器,合并时就会提示分片缺失。生产环境要么使用共享存储,要么通过网关或负载均衡保证同一个 MD5 路由到同一个实例。
合并成功后才能保存视频草稿
草稿里的 videoUrl 应该使用合并接口返回的正式地址,而不是本地分片地址或临时地址。否则后续转码服务无法稳定回源。
转码失败不等于上传失败
上传成功只代表原始视频已经进入 MinIO,并写入上传记录。转码是提交审核后的异步链路,如果 FFmpeg 路径、源文件下载、水印资源或编码兼容性出问题,视频会进入 TRANSCODE_FAILED,但原始上传记录仍然存在。
总结
这套视频切片上传链路把“文件可靠上传”和“视频业务处理”拆开了:文件服务负责把大文件稳定地变成一个可访问的原始视频地址,社区服务负责围绕这个地址做草稿、提交、转码、审核和发布。
从实现效果看,MD5 秒传、分片目录、缺失分片返回、MinIO 入库、完整源文件缓存这几个点串起来后,已经能覆盖大部分视频上传场景。后续如果要继续增强,可以考虑并发分片上传、分片级校验、上传进度持久化、多实例共享分片目录,以及转码任务的重试和补偿机制。