断点续传进度持久化:上传中断后从头开始?Redis 记录分片状态,秒级续传!
公司做视频平台,用户上传一个 2GB 的素材文件,进度条跑到 95%,浏览器崩了。刷新页面重新上传,进度条又从 0% 开始。用户直接关了页面,再也没回来。
后来加了断点续传。用户重新打开页面,后端一问 Redis——"这个文件你上次传了 38 个分片,还差 2 个",直接从第 39 片开始传。3 秒搞定,用户甚至没注意到断过。
大文件上传这种事,不怕慢,怕的是断了之后要从头再来。今天聊聊怎么用 Redis 记录分片上传状态,实现真正的秒级续传。
先说清楚:为什么大文件上传非得分片
一个 2GB 的文件你不可能一口气传上去。网络稍微波动一下,整个 HTTP 请求就废了,2GB 白传。
所以业内的标准做法是分片上传:客户端把文件切成小块(比如每片 5MB),一片一片发。服务端收齐所有分片后,按顺序合并成完整文件。
客户端:
文件 2GB → 切成 400 片,每片 5MB
上传过程:
上传第 1 片 ✓
上传第 2 片 ✓
...
上传第 399 片(网络断了!)✗
上传第 400 片 ✗
服务端:
收到 398 片
等待第 399、400 片(永远等不到)
分片之后,问题就变成了:怎么知道哪些片已经传了、哪些还没传?
方案演进:从客户端自报到家到 Redis 集中管理
最粗糙的做法:客户端自己记
客户端每上传一片就在本地存个状态。断了重来的时候,先看本地记录,把没传的片补上。
问题很明显:换个浏览器、清个缓存、换个设备,记录全没了。而且客户端不可信,它说"传完了"你就信?
进一步:数据库记
服务端收到一片就在 MySQL 里插一条记录。续传的时候查表,找到缺失的分片号。
能用,但有两个问题。一是上传是个高频写入操作,每片 5MB、总共 400 片就是 400 次 INSERT,对数据库压力不小。二是续传查询虽然能走索引,但这个场景天然更适合用缓存而不是关系型数据库。
最优解:Redis 记
分片状态的特点:临时、高频、键值对、有过期需求。
这简直是 Redis 的舒适区。
Redis 怎么记分片状态
核心思路:用 Redis 的 Bitmap 或 Set 记录每个文件的分片完成情况。
方案 A:Bitmap(省内存,推荐)
每个文件分配一个 Redis key,用一个 Bitmap 标记每个分片是否已上传。
文件 ID:file:upload:abc123
总分片数:400
上传第 27 片成功后:
Redis SETBIT file:upload:abc123 27 1
查询哪些片还没传:
for i in 0..399:
if Redis GETBIT file:upload:abc123 i == 0:
missingChunks.add(i)
一个 400 分片的文件,Bitmap 只占 50 字节。内存开销几乎可以忽略。
方案 B:Set(直观,适合调试)
如果想直观一点,也可以用 Set:
上传第 27 片成功后:
Redis SADD file:chunks:abc123 27
查询已传分片数:
Redis SCARD file:chunks:abc123 → 398
查询缺失分片:
全集 [0..399] - SMEMBERS file:chunks:abc123
Set 的好处是可以用 SMEMBERS 直接看到哪些片传了,排查问题的时候一眼就能看清楚。缺点是内存占用比 Bitmap 大一些。
完整的续传流程
把上面的逻辑串起来,一个完整的断点续传流程长这样:
==================== 初始化阶段 ====================
客户端:
POST /api/upload/init
Body: { fileName: "video.mp4", fileSize: 2147483648, chunkSize: 5242880 }
服务端:
fileId = UUID.randomUUID()
totalChunks = ceil(fileSize / chunkSize) // = 400
Redis:
SET file:upload:{fileId}:total {totalChunks}
SET file:upload:{fileId}:name {fileName}
EXPIRE file:upload:{fileId}:* 86400 // 24小时后自动清理
返回: { fileId: "abc123", totalChunks: 400 }
==================== 上传阶段 ====================
客户端:
POST /api/upload/chunk
Body: 第 N 片二进制数据
Header: X-File-Id: abc123, X-Chunk-Index: 27
服务端:
// 1. 把分片存到临时目录
write("/tmp/uploads/abc123/chunk_27", chunkData)
// 2. 标记分片已完成
Redis SETBIT file:upload:abc123 27 1
// 3. 返回当前进度
doneCount = Redis BITCOUNT file:upload:abc123
返回: { chunkIndex: 27, doneCount: 398, totalChunks: 400 }
==================== 续传阶段(断线重连后)=====================
客户端:
POST /api/upload/resume
Body: { fileId: "abc123" }
服务端:
totalChunks = Redis GET file:upload:abc123:total → 400
// 用 BITCOUNT 拿到已完成数
doneCount = Redis BITCOUNT file:upload:abc123 → 398
// 用 BITFIELD 或遍历找出缺失分片
missingChunks = []
for i in 0..totalChunks-1:
if Redis GETBIT file:upload:abc123 i == 0:
missingChunks.add(i)
返回: {
doneCount: 398,
totalChunks: 400,
missingChunks: [399, 400]
}
客户端:
只上传第 399、400 片 → 搞定
==================== 合并阶段 ====================
客户端:
POST /api/upload/merge
Body: { fileId: "abc123" }
服务端:
// 1. 校验:BITCOUNT 是否等于 totalChunks
if Redis BITCOUNT file:upload:abc123 != totalChunks:
return "分片不完整"
// 2. 按顺序合并所有分片
for i in 0..totalChunks-1:
读取 /tmp/uploads/abc123/chunk_{i}
追加写入最终文件
// 3. 清理
删除 /tmp/uploads/abc123/
Redis DEL file:upload:abc123*
这个流程里有一个容易被忽略的细节:初始化的时候不要检查文件是否已存在。 而是把"新建上传"和"续传"做成两个独立接口。客户端先尝试 /resume,如果返回 fileId 不存在,再走 /init 新建。这样逻辑更清晰,不会出现同一个接口做两件事的混乱。
三个细节决定体验好坏
分片太小,请求太密;分片太大,失败代价高
- 分片 1MB:一个 2GB 文件切 2000 片。HTTP 请求开销大,进度条更新太密。
- 分片 20MB:一个 2GB 文件只切 100 片。但如果第 99 片上传失败,20MB 白传。
一般 2~5MB 是个比较平衡的选择。具体可以根据业务场景调整——视频类可以大一点,弱网环境下调小一点。
Redis key 要设过期时间
上传状态是临时的。文件合并完之后这个状态就没用了。如果不设过期时间,Redis 里会堆满僵尸 key。
EXPIRE file:upload:{fileId}:* 86400 // 24 小时
一天的时间窗口足够覆盖绝大多数上传场景。如果业务允许的超时时间更长,可以适当延长。
合并的时候要按顺序读
分片上传的时候可能是多线程并发的,第 200 片可能比第 100 片先到。但合并的时候必须从 0 到 N 按顺序拼接,不然文件就坏了。
这个没啥技巧,就是个循环:
for i in 0..totalChunks-1:
data = read("chunk_" + i)
write(finalFile, data)
要不要引入消息队列?
如果你的上传量很小(每分钟几十个),上面的方案完全够用。
但如果是大并发场景——几百个用户同时上传,每个人 400 个分片,那每秒的 Redis 操作量会很大。而且合并操作是 IO 密集型的,同步执行会阻塞上传线程。
这时候可以把合并操作丢到消息队列里,异步处理:
客户端请求合并:
POST /api/upload/merge
服务端:
if BITCOUNT == totalChunks:
发送消息到 MQ: { fileId: "abc123" }
返回: "合并任务已提交"
消费者:
从 MQ 拿到 fileId
执行合并
更新 DB 文件状态
不过对于大多数场景来说,同步合并就够用了。别一上来就上 MQ,先看有没有这个必要。
总结
断点续传这件事,本质就两件事搞定:
分片上传 —— 大文件切成小块,每片独立上传,失败只影响一片。
状态持久化 —— 用 Redis Bitmap 记录每片的完成状态。BITCOUNT 查进度、GETBIT 找缺口、SETBIT 标记完成。三个命令搞完。
至于为什么用 Redis 而不是 MySQL,说白了就是——这个场景的数据特征跟 Redis 的定位完美匹配:临时数据、高频写入、简单键值查询、需要过期自动清理。MySQL 能干这事,但属于大材小用,还帮倒忙。
下次用户上传 2GB 文件断了重连,别再让人从头开始。
有用的话转给还在让用户重传 2GB 文件的后端。
标题:断点续传进度持久化:上传中断后从头开始?Redis 记录分片状态,秒级续传!
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/06/03/1780410849886.html
公众号:服务端技术精选
评论