
一、大文件切片上传怎么设计大文件上传我一般会拆成切片、并发上传、断点续传、秒传、失败重试和最终校验这几块核心是让上传可以暂停、失败后继续而且不用从头再传。面试官继续追问后的展开1. 为什么要切片如果一个 1.5GB 视频直接上传1.5GB File ↓ 一个 HTTP 请求 ↓ 上传失败 ↓ 基本只能重新上传切片之后1.5GB File ↓ ┌────┬────┬────┬────┬────┬────┐ │ 0 │ 1 │ 2 │ 3 │ ...│ N │ └────┴────┴────┴────┴────┴────┘ ↓ 分别上传 ↓ 服务端保存各个 chunk ↓ 全部完成 ↓ 按顺序合并前端核心 API 就是constchunkfile.slice(start,end);但slice()本身根本不是这道题的难点。真正的难点是切完以后怎么可靠地把这些 chunk 上传、恢复、校验并最终合并。2. 切片之后为什么不能直接Promise.all()这是第一个高频追问。比如awaitPromise.all(chunks.map(chunkupload(chunk)));代码虽然简单但实际上等于有多少切片就同时发多少请求。但不应该一次性把所有 chunk 都启动上传。如果一个 1GB 文件按 1MB 切就是大约 1024 个请求。这会带来浏览器连接和请求调度压力服务端瞬时并发压力带宽竞争大量 Promise 和请求对象某个网络环境下整体吞吐反而下降所以实际应该做固定并发数 任务队列。例如chunk queue │ ┌───────┼───────┐ ↓ ↓ ↓ worker1 worker2 worker3 ↓ ↓ ↓ chunk0 chunk1 chunk2 ↓ 完成后继续取下一个例如限制constCONCURRENCY4;4 个请求完成一个再从队列取下一个。这里还有一个容易被问的点并发数不是固定的“46 个最佳”。它取决于HTTP 版本浏览器网络质量chunk 大小服务端处理能力带宽是否走 CDN / 对象存储所以高级回答不要说HTTP/1.1 最多只能 6 个所以设置 6。更准确的说法是HTTP/1.1 同源连接数受浏览器限制而 HTTP/2 可以在单连接上复用多个请求实际并发数还是应该根据网络和服务端吞吐做控制而不是死记一个数字。3. 怎么实现断点续传这是这道题真正开始拉开差距的地方。错误思路浏览器 localStorage ↓ 记录 chunk 0、1、2、3 已上传这个方案只能解决同一台设备、同一个浏览器、缓存没有被清掉的情况下恢复。但它解决不了清缓存 换浏览器 换电脑 换手机 重新登录所以断点续传真正应该以服务端状态为准。正确流程用户选择文件File ↓ 计算 fileHash ↓ POST /upload/check │ ├── 文件已经存在 → 秒传 │ └── 文件不存在 ↓ 返回已上传 chunks ↓ 前端过滤已上传 chunk ↓ 只上传缺失 chunk例如服务端返回{uploadedChunks:[0,1,2,5,6]}那么0 ✓ 1 ✓ 2 ✓ 3 → 上传 4 → 上传 5 ✓ 6 ✓ 7 → 上传这样网络断在 99%重新上传的时候不是从 0 开始而是继续上传缺失部分。4. 服务端怎么知道这是哪个文件这里就需要一个非常关键的概念文件唯一标识通常可以基于fileHash再结合chunkIndex来定位 chunk。例如fileHash abc123 chunk: abc123_0 abc123_1 abc123_2 ...或者服务端使用uploadId chunkIndex这种设计通常更稳妥。因为文件身份和一次上传任务的身份不一定应该完全绑定在一起。例如同一个文件fileHash abc123可能同时出现uploadIdA uploadIdB这就涉及后面的并发上传同一个文件问题。5. 秒传到底是什么秒传本质上不是“上传很快”。而是服务端发现自己已经有这个文件所以直接复用已有文件不再上传文件内容。流程用户选择文件 ↓ 计算 fileHash ↓ POST /upload/check ↓ 服务端查询 fileHash ↓ 存在 ↓ 直接返回成功所以1.5GB 文件 ↓ 只需要上传/计算文件指纹 ↓ 服务端发现已有 ↓ 直接完成这才叫秒传。6. 那么 fileHash 怎么算这里又是一个很容易被忽略的性能问题。如果你直接读取整个 1.5GB 文件 ↓ 计算 MD5就可能造成CPU 消耗主线程卡顿内存压力用户界面卡顿所以实际可以File ↓ 分块读取 ↓ Hash Worker ↓ 计算 fileHash也可以使用Web Worker把计算放到 Worker 中避免阻塞主线程。7. 文件 Hash 和 Chunk Hash 是一回事吗不是。可以分别存在fileHash和chunkHash例如fileHash SHA256(file) chunk0Hash SHA256(chunk0) chunk1Hash SHA256(chunk1) chunk2Hash SHA256(chunk2)这样服务端可以进一步校验上传 chunk ↓ 服务端计算/校验 chunkHash ↓ 一致 → 接收 不一致 → 重新上传这个 chunk这样比等到整个文件合并之后才发现错误更加高效。8. 如果某一个切片上传失败怎么办不能因为一个 chunk 失败chunk 0 ✓ chunk 1 ✓ chunk 2 ✗ chunk 3 ✓ chunk 4 ✓然后全部重新上传应该只重试失败的 chunk。例如chunk2 ↓ 失败 ↓ retry 1 ↓ 失败 ↓ retry 2 ↓ 失败 ↓ retry 3 ↓ 仍然失败 ↓ 任务失败重试一般配合指数退避避免网络异常的时候大量请求同时再次打到服务端。9. 网络断了怎么恢复这里其实有两种情况。情况一页面没刷新前端可以保留当前上传任务状态uploadId fileHash uploadedChunks网络恢复之后重新查询服务端 ↓ 获取最新 uploadedChunks ↓ 继续上传缺失 chunk情况二页面刷新甚至换设备这时候前端自己的内存状态全部没了。所以仍然依赖fileHash ↓ 服务端查询 ↓ 返回已经存在的 chunks ↓ 继续上传这也是为什么localStorage 最多只能做客户端辅助缓存不能作为断点续传的最终依据。10. 两个用户同时上传同一个文件怎么办这是非常好的追问。例如User A ── upload ──┐ ├── fileHash abc123 User B ── upload ──┘服务端不能简单认为A 创建文件 B 又创建一个完全相同的文件应该围绕fileHash做去重。例如fileHash abc123 ↓ 服务端发现已经存在 ↓ 直接复用但这里又有一个更深的问题文件可能正在上传还没有最终完成。例如User A chunk 0 ✓ chunk 1 ✓ chunk 2 ✓ ... User B ↓ 发现 abc123 正在上传 ↓ 复用已有上传进度这就涉及upload session状态机并发控制幂等数据库唯一约束分布式锁 / CAS11. Chunk 上传接口为什么必须幂等例如POST /upload/chunk网络情况可能是客户端发送 chunk ↓ 服务端已经成功保存 ↓ 响应丢失 ↓ 客户端以为失败 ↓ 再次上传同一个 chunk如果服务端没有幂等设计就可能重复保存所以应该让uploadId chunkIndex或者fileHash chunkIndex成为唯一定位。再次上传相同 chunk 时已经存在 → 直接返回成功 不存在 → 保存这就是高级工程设计里非常重要的幂等性。12. 所有 Chunk 都上传完之后呢这时候才进入Merge流程chunk0 ✓ chunk1 ✓ chunk2 ✓ ... chunkN ✓ ↓ POST /upload/complete ↓ 服务端确认所有 chunk 都存在 ↓ 按 chunkIndex 顺序合并 ↓ 生成最终文件注意前端不能简单告诉服务端“我上传完了”服务端应该自己再次确认。13. 合并的时候怎么保证顺序例如chunk0 chunk1 chunk2 chunk3必须按照0 → 1 → 2 → 3合并。不能按照上传完成顺序2 → 0 → 3 → 1因为上传顺序和文件顺序是两回事。这也是为什么每个 chunk 必须带chunkIndex14. 合并完成后怎么校验完整性这是非常关键的一问。最直接的方案是原文件 fileHash ↓ 上传 合并 ↓ 服务端对最终文件计算 hash ↓ 比较 ↓ 一致 → 成功 不一致 → 异常例如Client: SHA-256(originalFile) ↓ abc123 Server: SHA-256(mergedFile) ↓ abc123 ↓ 一致 ↓ 上传成功15. 如果最终 Hash 不一致是全部重传还是部分重传不能简单地说“部分重传”。因为fileHash 不一致只能说明最终文件和原文件不一致。它本身不能直接告诉你“到底是 chunk 37 错了。”如果每个 chunk 都有chunkHash服务端就可以进一步定位chunk0 ✓ chunk1 ✓ chunk2 ✓ chunk3 ✗ ...那么只重新上传 chunk3。所以更完整的设计是fileHash │ ↓ 整体完整性校验 │ 不一致 │ ↓ chunkHash 对比 │ ┌────────┴────────┐ ↓ ↓ 找到异常 chunk 无法定位 ↓ ↓ 部分重传 整体重新处理16. 合并是不是一定要同步完成不一定。如果是10MB 文件同步合并可能完全没问题。但如果是10GB 50GB 100GB合并可能持续很长时间。这时候更合理的是POST /upload/complete ↓ 创建 merge task ↓ 立即返回 ↓ 后台异步合并 ↓ 状态merging ↓ 完成 ↓ 状态completed前端可以轮询或者WebSocket SSE接收状态变化。注意WebSocket/SSE 不是大文件上传本身的必要条件只是用于把异步合并状态通知给前端。17. 整个链路怎么串起来这是这道题最值得背下来的一张图用户选择文件 │ ↓ 计算 fileHash │ ↓ /upload/check │ ┌──────────┴──────────┐ ↓ ↓ 文件已经存在 文件不存在 ↓ ↓ 秒传 返回 uploadId │ ↓ 查询已上传 chunks │ ↓ 过滤已上传 chunk │ ↓ 并发队列上传 │ ┌─────────────────┴──────────────┐ ↓ ↓ 成功 失败 │ │ │ 自动重试/指数退避 │ │ └──────────────┬─────────────────┘ ↓ 全部 chunk 完成 │ ↓ /upload/complete │ ↓ 服务端合并 │ ↓ 最终文件完整性校验 │ ┌─────────┴─────────┐ ↓ ↓ 一致 不一致 ↓ ↓ 成功 定位异常 chunk ↓ 重传18. 这道题真正考什么可以把面试官的追问链压缩成会切片 ↓ 会并发控制吗 ↓ 会断点续传吗 ↓ 换电脑还能续传吗 ↓ 能秒传吗 ↓ 两个用户同时上传呢 ↓ 请求重试会不会重复 ↓ 合并怎么保证顺序 ↓ 怎么校验完整性 ↓ 校验失败怎么定位 ↓ 大文件合并会不会阻塞 ↓ 异常状态怎么恢复所以它考的其实不是Blob.slice()会不会。而是你能不能把一个“文件上传 API”设计成一个可靠的端到端系统。19. 面试官最容易继续追问的几个点追问 1localStorage能不能实现断点续传不能作为最终方案。它只能保存客户端状态清缓存、换浏览器、换设备都会丢。真正的上传进度应该以服务端已保存的 chunk 状态为准。追问 2为什么不用Promise.all()因为切片数量可能非常大不能让所有请求同时发出去要用并发队列控制同时上传的数量。追问 3为什么需要chunkIndex因为上传完成顺序是不确定的服务端必须靠chunkIndex恢复文件原来的顺序。追问 4为什么需要fileHash主要解决两个问题1. 判断是不是同一个文件 2. 实现秒传和断点续传追问 5为什么还需要chunkHash因为fileHash只能告诉你最终文件对不对chunkHash可以进一步定位到底哪个切片有问题。追问 6上传接口为什么要幂等因为请求成功 ↓ 响应丢失 ↓ 客户端重试可能导致同一个 chunk 被重复上传。所以服务端要能够安全处理重复请求。追问 7HTTP/2 之后还需要控制并发吗需要。HTTP/2 解决的是多个请求共享连接的问题但不代表服务端、浏览器和网络可以无限并发。并发控制依然是为了控制带宽 CPU 内存 服务端压力 请求队列20. 最后给你一版真正适合面试开口的答案大文件上传我一般不会只考虑“切片”而是把它做成一套完整的上传链路切片、并发控制、断点续传、秒传、失败重试、合并和完整性校验。前端先用Blob.slice()把文件切成多个 chunk每个 chunk 带上uploadId、chunkIndex等信息然后通过并发队列控制同时上传的请求数量不能直接Promise.all()把所有切片一起发出去。断点续传不能只依赖localStorage真正应该以服务端状态为准。用户重新上传时先根据fileHash查询服务端已经有哪些 chunk只上传缺失的部分所以即使刷新页面、清缓存甚至换一台电脑也能继续。秒传也是基于fileHash服务端如果已经存在这个文件就直接复用已有文件不需要再次上传内容。上传过程中单个 chunk 失败只重试这个 chunk并且接口要保证幂等避免网络重试造成重复数据。所有 chunk 上传完成后再通知服务端按chunkIndex顺序合并。合并完成后校验最终文件的fileHash如果不一致再通过chunkHash定位异常 chunk优先做部分重传。所以这道题真正考的不是会不会slice()而是能不能把大文件上传做成一个可恢复、可重试、可校验、能处理并发和异常的完整方案。