一、大文件切片上传怎么设计?
大文件上传我一般会拆成:切片、并发上传、断点续传、秒传、失败重试和最终校验这几块,核心是让上传可以暂停、失败后继续,而且不用从头再传。
面试官继续追问后的展开
1. 为什么要切片?
如果一个 1.5GB 视频直接上传:
1.5GB File ↓ 一个 HTTP 请求 ↓ 上传失败 ↓ 基本只能重新上传切片之后:
1.5GB File ↓ ┌────┬────┬────┬────┬────┬────┐ │ 0 │ 1 │ 2 │ 3 │ ...│ N │ └────┴────┴────┴────┴────┴────┘ ↓ 分别上传 ↓ 服务端保存各个 chunk ↓ 全部完成 ↓ 按顺序合并前端核心 API 就是:
constchunk=file.slice(start,end);但slice()本身根本不是这道题的难点。
真正的难点是:
切完以后,怎么可靠地把这些 chunk 上传、恢复、校验并最终合并。
2. 切片之后为什么不能直接Promise.all()?
这是第一个高频追问。
比如:
awaitPromise.all(chunks.map(chunk=>upload(chunk)));代码虽然简单,但实际上等于:
有多少切片就同时发多少请求。但不应该一次性把所有 chunk 都启动上传。
如果一个 1GB 文件按 1MB 切,就是大约 1024 个请求。
这会带来:
- 浏览器连接和请求调度压力
- 服务端瞬时并发压力
- 带宽竞争
- 大量 Promise 和请求对象
- 某个网络环境下整体吞吐反而下降
所以实际应该做:固定并发数 + 任务队列。
例如:
chunk queue │ ┌───────┼───────┐ ↓ ↓ ↓ worker1 worker2 worker3 ↓ ↓ ↓ chunk0 chunk1 chunk2 ↓ 完成后继续取下一个例如限制:
constCONCURRENCY=4;4 个请求完成一个,再从队列取下一个。
这里还有一个容易被问的点
并发数不是固定的“4~6 个最佳”。
它取决于:
- 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可能同时出现:
uploadId=A uploadId=B这就涉及后面的并发上传同一个文件问题。
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
- 状态机
- 并发控制
- 幂等
- 数据库唯一约束
- 分布式锁 / CAS
11. 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. 面试官最容易继续追问的几个点
追问 1:localStorage能不能实现断点续传?
不能作为最终方案。
它只能保存客户端状态,清缓存、换浏览器、换设备都会丢。
真正的上传进度应该以服务端已保存的 chunk 状态为准。
追问 2:为什么不用Promise.all()?
因为切片数量可能非常大,不能让所有请求同时发出去,要用并发队列控制同时上传的数量。
追问 3:为什么需要chunkIndex?
因为上传完成顺序是不确定的,服务端必须靠chunkIndex恢复文件原来的顺序。
追问 4:为什么需要fileHash?
主要解决两个问题:
1. 判断是不是同一个文件 2. 实现秒传和断点续传追问 5:为什么还需要chunkHash?
因为fileHash只能告诉你最终文件对不对,chunkHash可以进一步定位到底哪个切片有问题。
追问 6:上传接口为什么要幂等?
因为:
请求成功 ↓ 响应丢失 ↓ 客户端重试可能导致同一个 chunk 被重复上传。所以服务端要能够安全处理重复请求。
追问 7:HTTP/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(),而是能不能把大文件上传做成一个可恢复、可重试、可校验、能处理并发和异常的完整方案。