大文件分片上传实战:断点续传、秒传与时序图设计

发布时间:2026/10/7 3:37:41
大文件分片上传实战:断点续传、秒传与时序图设计 1. 为什么大文件上传必须走分片从一次失败的直传说起先讲个真实的场景。之前我负责的项目里用户需要上传单个 2GB 左右的视频文件到云端做转码。第一版图省事前端直接把文件整个塞进 FormData后端接口也简单接收完写盘就完事。结果上线第三天就被用户投诉传了半小时一下断网全没了重新传了三次每次进度条都从零开始——这个反馈直接把直传方案打回原形。大文件直传的致命问题在于网络从来不是稳定传输的通道。一个 2GB 的文件哪怕你在办公网络下看起来很快中间任何一次断流、浏览器崩溃、后端超时整个传输都得推倒重来。而且 HTTP 协议对大体积请求并不友好网关、代理层动不动就会主动断开长时间运行的连接。更别提移动端切后台、锁屏导致网络挂起之类的情况一次上传的生命周期太脆弱了。分片上传的核心思路其实跟拆快递箱子差不多一个大箱子难搬那就拆成几个小箱子逐个搬到了目的地再重新组装。技术上就是把文件按固定大小切成多个块每个分片通常几 MB前端逐个上传后端逐个保存全部传完后触发合并把分片拼回完整的文件。但分片只是表象真正有价值的是它带来的三个能力断点续传——传到一半断了记录下已传完的分片下次直接从断处继续并发加速——多个分片并行上传充分利用带宽能明显缩短整体耗时秒传——通过文件的唯一标识和服务端已存在的记录比对直接跳过上传。这三个能力直传方案一个都做不到。不过也要泼一盆冷水分片上传引入的复杂度是实打实的不是每个项目都需要。几十 MB 的小文件用分片纯粹是给自己找事多一次合并逻辑就多一份出错风险。我一般建议文件超过 100MB 再考虑分片或者你的产品明确有弱网环境、移动端上传、断点续传需求时再上。怎么判断看两个维度文件是否经常超过百兆级别以及用户是否经常在非稳定网络下操作。两个都答是就值得做。2. 分片上传系统的角色分工前端不只是一个搬运工很多初学的人以为分片上传就是前端切块、后端拼起来这么简单等真正做起来才发现里面还藏着一个隐形的角色——哪个部分负责记录哪些分片已经传过了。如果这个职责没理清断点续传就会变成一句空口号。2.1 前端的职责切分、标识、调度与重试前端是整个分片上传的发起方但不是单纯地把文件切开然后循环请求。一个负责任的浏览器端需要处理这几件事第一切分文件并生成分片标识。浏览器里最方便的工具就是Blob.slice()它可以直接从 File 对象中切出指定字节范围的子块不需要把整个文件读进内存。拿到子块之后每个分片要有一个能唯一标识自己的编号通常是文件名分片序号更严谨的做法是给整个上传任务生成一个全局唯一的 taskId。第二计算文件的唯一指纹。这个指纹一般用 MD5、SHA-1 或更安全的 SHA-256 来实现计算方式是对整个文件内容做哈希。这样做的目的有两个一是服务端可以通过指纹判断这个文件是否已经存在秒传二是可以校验分片合并后是否损坏。但注意对整个 2GB 文件做哈希可不是瞬时的在浏览器里用 SparkMD5 这类库去算通常会做个增量哈希边读边算耗时跟网络传输差不多所以很多实现会先把哈希放到后台算等哈希算完再开始上传或者干脆上传与哈希并行。第三控制并发和重试策略。把 100 个分片一次性全发出去浏览器连接池会先崩溃后端压力也扛不住。常规做法是限制并发数比如 3 到 5 个分片同时上传。每个分片请求失败后还需要自动重试指数退避是个好策略第一次失败等 1 秒、第二次等 2 秒、第三次等 4 秒最多重试 5 次这样的节奏。第四实时记录上传进度并展示给用户。分片上传的进度计算不能简单依赖单个 XMLHttpRequest 的进度事件因为并发请求会有多个进度回调交叉触发正确的做法是维护一个已成功上传分片数 / 总分片数的计数基于计数去算百分比。2.2 后端的职责登记、接收、落盘与合并后端在分片上传里承担的不只是收快递这么简单。通常要设计两个类型的接口一个是初始化接口前端在分片前先调用它创建上传任务。这个接口接收文件名、文件大小、总片数、分片大小以及最重要的文件指纹。后端拿到这些信息后先查一下这个指纹是否已经有对应文件如果有直接返回该文件已存在不需要上传的秒传响应如果没有则在数据库里新建一条上传任务记录状态为待上传。另一个是分片上传接口负责接收每个分片。这个接口需要校验三件事任务是否存在、当前分片的序号是否在合法范围内、分片的内容大小是否符合预期。校验通过后分片内容写入磁盘上的临时文件建议按taskId/序号来组织方便后续合并时按序号遍历。每次上传成功更新数据库里该任务的分片状态方便断点续传时查询。最后还有一个合并接口在所有分片上传完成后被调用。后端从临时目录中读取该任务的全部分片按序号顺序把内容追加到目标文件里。合并完成后要做一次完整性校验——比较合并后文件的大小与创建任务时记录的大小是否一致。条件允许的话再做一次文件级哈希比对因为某个分片在传输或落盘过程中被篡改或损坏时光比大小是发现不了的。2.3 为什么必须引入任务的概念直传方案不需要任务这个中间态因为请求本身就是一次性的。分片上传则不同一次完整的上传被拆成了几十次甚至上千次独立的网络请求如果不建立一个任务来串联这些请求的上下文你根本无从得知哪些分片已经传完了哪些丢了合并什么时候可以触发任务其实就是整场上传协作的会议纪要。两端都围绕它做状态同步前端查任务详情知道接下来该从哪个分片续传后端根据任务状态判断是否可以合并。没有任务概念的分片上传跟没有总指挥的施工队一样各干各的最后拼不到一块儿。参与角色核心职责关键产物浏览器前端文件切分、哈希计算、并发调度、重试与进度反馈任务 ID、分片集合、文件指纹后端接口层任务初始化、分片校验与接收、状态维护任务记录、分片落盘状态后端合并层读取全部分片、按序合并、完整性校验合并后的完整文件存储层临时分片存储、最终文件持久化分片文件、目标文件3. 整套流程的时序推进从创建任务到文件合并理解角色分工之后就要把这些角色放进时间轴里看什么时刻谁主动发起动作、触发谁、传什么数据。这就是时序图存在的意义——它不是一个画给老板看的装饰图而是整个系统的行为契约。我用一个典型的成功流程来拆解完整的时间线。3.1 第一阶段准备与任务创建前端先做文件切分和哈希计算。这里有一个容易被忽略的顺序问题哈希计算是否必须在上传前完成严格来说只有在优先尝试秒传或上传过程中校验完整性时才必须在任务初始化之前拿到完整指纹。如果你的产品不做秒传也不想耗时做哈希那完全可以跳过去让任务创建更轻快。随后前端向后端发起初始化请求完整地提交这些元数据{ taskId: 8f3a2c1e-..., fileName: console_demo.mp4, fileSize: 2057981306, chunkSize: 5242880, chunkCount: 393, fileHash: d41d8cd98f00b204e9800998ecf8427e }后端收到后执行三个动作查哈希判断是否可以秒传、创建任务记录、预估存储空间。如果前进方向是秒传直接返回跳过上传的通知否则返回uploadUrl、允许的分片大小、过期时间等参数告诉前端可以开始传了。3.2 第二阶段分片并发上传与状态同步任务建立后前端进入核心循环按序号取出分片在并发上限内发请求。每个分片请求的核心参数如下POST /api/v1/uploads/{taskId}/chunks Content-Type: multipart/form-data chunkIndex: 0 totalChunks: 393 chunkFile: (binary)后端每收到一个分片完成校验后落盘然后更新数据库。这个更新动作要放到整个请求的末尾特别注意事务边界。我见过一个非常典型的 bug先更新数据库再落盘结果磁盘空间不足导致写文件失败数据库里却已经标记为已上传后续断点续传就永远跳过了这个分片合并时才暴露文件损坏。前端是否每个分片都等待服务端确认不需要。并发上传时每个分片独立收到响应前端维护一个已确认分片集合每当一个分片确认成功就从待传队列里移除。至于已传分片记录服务端应该在初始化接口之外提供一个查询已传分片列表的接口断点续传时前端拉取一次就能精确知道跳过哪些分片、从哪里继续。这期间的时序非常密集前端与后端之间是多路并发 独立确认的关系网络层的重试、超时也在同时发生。时序图上表现出来就是多条并行的箭头各走各的最终汇总到同一个任务状态表里。3.3 第三阶段触底合并与完整性校验最后一个分片上传成功的响应返回前端后前端调用合并接口。为了防御极端情况合并接口内部必须再加一道门槛先查询任务的分片完成状态如果存在未标记完成的分片直接拒绝合并并返回缺少的分片序号列表。为啥要在后端判断而不相信前端因为请求顺序无法保证前端认为发完了不等于后端收完了尤其是并发请求丢包后重试过程中奇偶分片的到达顺序完全可能错位。合并本身是顺序操作遍历序号 0 到 N逐个读取临时分片并写入目标文件。这里的性能点在于尽量不要用read() write()在应用层拷贝更高效的方式是使用FileChannel.transferTo()或者sendfile这类内核态零拷贝操作减少内存占用。合并完成之后删除临时分片目录更新任务状态为合并完成。最后一步完整性校验不能省。比较最终文件大小和任务里记录的 fileSize是一种低成本快速验证如果上传前做了文件级哈希合并后服务端再算一次哈希与原值比对质量可信度就非常高。至此一次完整的分片上传生命周期结束。整个流程从创建任务到合并校验所有关键节点都以任务状态为锚点前后端每一步的交互时机都是可预期的。这就是时序图要承载的内容交互必须在这个时刻发生而不是在任意时刻。4. 时序图这样画才有价值谁在什么时刻做了什么既然聊的是分片上传时序图那就得把时序图这件事本身讲透。很多人画时序图就是拿几个框和箭头把请求发出来画完别人看不懂自己也看不懂就是因为没搞清时序图的语法核心其实是在表达时间上的先后约束。4.1 四要素参与者、生命线、消息、激活块一个标准 UML 时序图最基本的组成是四个东西。参与者Actor是系统边界外的触发者在分片上传场景里就是浏览器用户生命线Lifeline是每一个参与交互过程的实例比如前端页面、文件切片器、后端的接口、数据库、存储服务每个实例都有自己的生命线消息Message就是它们之间传递的调用或信号包括同步请求、异步回调、返回结果激活块Activation表示参与者处理某个消息期间占据的时间区间在图上就是一条竖长的矩形。举个最简单的例子前端请求初始化接口时前端生命线发出一条带实心箭头的消息指向后端接口生命线后端的生命线上出现一个激活块表示它在处理这个消息处理完一条虚线箭头画回来表示返回响应激活块结束。这个过程就是时序图最小单位的语义闭环。分片上传的时序图复杂在它不是单一调用链而是多阶段、多并发、多状态反馈的组合。画的时候如果所有消息全塞一张图上结果只可能是一团乱麻。我建议按阶段拆分成多个交互图每个图表达一个清晰的目标。4.2 用一张任务创建时序图把关键交互钉死以第一阶段为例完整消息顺序如下前端 → 文件读取器读取文件元数据文件名、大小。文件读取器 → 哈希计算器开始增量计算文件指纹。前端 → 上传服务POST /init文件名、总大小、总分片数、文件指纹。上传服务 → 数据库查询该文件指纹是否已有记录。数据库 → 上传服务返回记录若存在已完成文件的分片信息。上传服务 → 前端返回任务已创建 | 秒传命中。若命中秒传前端 → 用户提示文件已存在上传完成。这张图的价值在于它把秒传判断发生在任务创建阶段这一顺序约束画得清清楚楚。如果有人在实现时把秒传判断放到合并阶段才做图上是直接看得出的——因为消息的顺序不符合图上定义。4.3 并发分片上传的时序怎么表达并发分片上传时时序图要表达的核心命题是多路请求同时进行但服务端能接受乱序到达并逐个确认同时前端限制并发数量。画法上可以用一条前端生命线同时发出多条实线消息分别指向后端生命线。要注意的是并行消息在时序图上允许上下并列但不代表它们都从同一时刻出发——更严谨的表达是在前端生命线上画一个 fork 操作符常见做法是把并发消息的水平位置稍微错开或加上parallel标签说明这组消息是并行的。在分片上传的时序图里后端生命线上对每个分片消息都应该画出独立的激活块它们可能重叠但各自独立。这样画读图的人一眼就能看出后端对每个分片请求的处理互不依赖合并动作必须等待所有这些激活块全部结束之后才能启动。4.4 错误分支和重试时序图不只是画晴天的最容易让时序图失真的是只画正常路径。真实系统中的断线、超时、重试恰恰是分片上传最需要表达清楚的地方。例如某个分片上传超时前端自动重试的时序片段至少需要画出前端 → 后台上传服务发送第 32 号分片请求。上传服务等待响应超时无返回。前端记录失败等待 1 秒。前端 → 上传服务重试发送第 32 号分片请求。上传服务 → 数据库查询第 32 号分片的已接收状态。数据库 → 上传服务返回未完成。上传服务 → 前端接收成功确认。注意第 5 步很重要。重试时服务端不能无条件接收分片而要先查状态。因为存在这种概率极低但确实可能发生的场景上一次请求其实已经写盘成功了只是响应在网络中丢失前端才误以为超时。这时服务端直接判定重复分片已存在返回幂等结果。这个逻辑画进时序图里就明确了重试必须幂等的约束靠聊天式口头沟通很容易漏掉这种细节。4.5 硬件时序图的启发信号边沿与状态对齐你可能好奇开头提到的那几个热词比如 I2C 时序图跟分片上传有什么关系。硬件领域的时序图讲究每个信号的电平变化发生在哪个边沿、数据在哪个时钟沿被采样差一个时钟周期设备就对不齐了。软件系统的时序图虽然没那么严苛但思想完全一致通信双方必须对什么时候发、什么时候收、什么时候确认达成一致否则就是对不上时钟。分片上传里面其实也有一个时钟对齐问题——客户端和服务端对任务状态的认知必须同步。前端按序号发了 100 个分片后端如果只记了 99 个两边就失步了。所以不只是画时序图要有对齐意识接口设计上也要有状态查询这样的对齐机制让前端随时可以问服务端你看到哪些分片了服务端如实回答两边一对比该补齐的补齐该重传的重传。这一切用画图工具表达时我的习惯是先用文本描述方式把每个阶段的消息列出来再落成图形。很多工具支持从文本描述反向生成时序图比如 PlantUML 和 Mermaid 就是比较常用的一类不过要注意一些内部文档平台对 Mermaid 的支持程度不同接地气的做法是落地成标准绘图文件或 SVG 图片。名字不重要重要的是你画出的每一根箭头的顺序和逻辑都是经得起推敲的。5. 断点续传与秒传真正让用户觉得爽的功能都在时序细节里分片的好处最终要落到用户能感知的功能上。断点续传和秒传是最能提升大文件上传体验的两件事但它们的实现恰好也是依赖时序图去梳理的重点。5.1 断点续传保存进度之后怎么续断点续传的底层逻辑一句话就能说清文件已经被切成若干分片每个分片有独立的上传状态已上传的就不需要再传。用户上传到一半断网或手动暂停下次重新选择同一个文件系统通过任务 ID 从服务端拉取已传分片列表前端跳过这些分片继续传剩下的。但这里有一个很常见的实现陷阱不能只靠文件名来定位任务。不同目录下可能存在同名文件同名的不同版本也会变。所以任务定位必须靠文件指纹文件大小任务 ID的组合其中文件指纹又是最关键的一环。那么断点续传的时序图就多了一个阶段前端重新拿到文件后要做一次和首次上传一致的哈希计算然后携带文件指纹发起查询任务请求。服务端返回该指纹关联的最近一个活动任务以及它的分片完成集合。前端对着这个集合算出差集从第一个缺失的分片继续传起。这一段交互时序图上新增了查询残留任务的消息逻辑清晰实现时也不容易漏。5.2 秒传哈希不是可选项而是必答题秒传的体验是你把文件拖进来瞬间弹出上传成功。本质是服务端发现该文件的指纹在数据库里已经存在于是直接关联到已有文件不再接收任何分片内容。这意味着哈希计算不仅要可靠还要在任务初始化前完成。前端拿到 File 对象后就得立刻启动哈希计算。如果计算耗时超过几秒用户体验上会有卡住的假象所以通常要加个进度提示正在计算文件指纹。文件越大计算越慢这里可以和初始化任务并联发起等哈希出来后再补发一条尝试秒传的请求实现上也不复杂。不过要特别注意秒传判断基于哈希哈希碰撞虽然概率极低但对分秒必争的企业场景是要有预案的。正规做法是哈希命中后不直接返回成功而是再比一次文件大小如果大小不一致就必须回退到普通分片上传流程。时序图上这个分支是哈希命中 大小匹配两者同为真的条件下才允许走秒传路径。5.3 状态流转的时序表达任务状态机先于代码设计把任务状态列成一个简单状态机待创建 → 已创建 → 上传中部分分片完成→ 合并中 → 已完成另外还有异常态合并失败和分片过期。时序图的每个消息本质上都在驱动这些状态之间的迁移。比如查询已传分片能触发从已创建向上传中的状态切换合并请求成功触发从上传中向合并中再向已完成的切换。画时序图时我习惯顺手把状态机的迁移条件标注在消息旁边。很多人说时序图画完没多久就和代码对不上了主要原因就是没有把状态变化固化到图上代码改了图还是旧的图对代码的约束力也就消失了。6. 真实项目中的坑合并顺序、幂等性、临时文件与并发数的取舍把时序图画明白、流程想清楚只能算是纸上谈兵的合格。真到了落地实现各种反直觉的坑会陆续冒出来。下面是几个我从实际项目中踩出来的经验写了这么多最值钱的部分其实在这里。6.1 并发到底开多大不是越大越好很多人以为并发开得越高上传越快。实际上当并发超过某个阈值后总耗时反而会上涨。背后的原因有几个每一条连接都要消耗服务端的文件句柄和线程资源TCP 拥塞控制会在并行连接之间竞争带宽浏览器对同一主机的最大连接数也有限制。实测里5MB 分片、总文件 1GB、并发数从 3 提到 5总耗时差别不太大提到 10某些弱网络下反而变慢。我的经验值是内网环境并发可以开到 5-6公网弱网环境控制在 3 就够。分片大小也是同理太大如 50MB会失去断点续传的粒度太小如 1MB会出现分片数过多、请求排队严重、数据库记录疯涨的副作用。常用区间是 2MB-10MB我一般选 5MB。6.2 幂等性重试不是重复发一次而是确保结果一致前面提到过重试幂等。这条我再展开说因为做不好会导致数据错乱。后端接收分片的接口在落盘前必须先查任务状态表看该分片是否已经存在并确认成功。如果存在直接返回成功而不重复写入如果存在但状态是写入失败这种中间态要允许覆盖重写。这个逻辑没有多复杂但很多人会忘。忘了的后果就是断点续传之后合出来的文件大小是对的某些分片却可能被重复写了两遍导致合并后二进制错位。6.3 临时分片管理的生命周期合并完成后临时分片必须删除这看似废话但实际出问题都出在没设置过期兜底。比如任务创建了用户传了一半就丢在那里不管或者在合并失败后没有清理机制一两年过去磁盘被临时文件塞满。成熟的方案是给任务设置有效期比如 24 小时或 7 天内相关分片有效过期未合并的自动清理合并失败的临时文件也要有重试清理的兜底。这些在时序图上不需要画得太细但在系统的背景任务里必须存在。6.4 合并时最容易被忽视的问题索引偏移分片序号看起来只是数字顺序但如果你做断点续传前端传给服务端的总片数可能在重试时是不一样的——例如用户换了一个前端版本分片大小配置发生变化同样的文件切出来的片数就不同。代表性的翻车现场任务 A 在小版本分片大小下创建用户清理了缓存前端版本更新后继续上传新前端按新分片大小切片序号对不上服务端期望的字节范围合并后文件头尾错乱。应对方法创建任务时把分片大小、每片的字节范围固定下来写入任务记录中后续任何一次分片上传服务端都校验当前分片对应的startByte和endByte与任务记录一致不一致直接拒绝。序列号只是索引字节范围才是硬约束这条经验值一套损失足以让你对硬件时序图里边沿对齐这四个字产生共鸣。6.5 上传中断后重新选择文件的匹配规则最后一个小细节用户断点续传时重新选择的文件不一定是同一个。产品上应该做指纹 大小双匹配如果指纹变了就要废掉原任务创建新任务。如果只是大小一样但内容不同指纹自然不同也不应该续传旧任务。这个判断做不对会让用户上传成功后拿到的文件实际上是他几天前上传的老版本。这些坑没有一个是靠天马行空的灵感想出来的全是在真实部署、线上反馈和监控报表里爬出来的。回到时序图这件事我的体会是画图只是手段真正的价值在于它强迫你把每一条消息、每一个状态迁移、每一个失败分支都想清楚再动手。把这张图钉在项目文档里让它成为前后端协作的契约比贴一大堆 API 文档更管用。