惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
月光博客
月光博客
博客园 - 司徒正美
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
量子位
Recent Announcements
Recent Announcements
V
V2EX
P
Proofpoint News Feed
小众软件
小众软件
云风的 BLOG
云风的 BLOG
腾讯CDC
宝玉的分享
宝玉的分享
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
A
About on SuperTechFans
B
Blog
博客园_首页
GbyAI
GbyAI
博客园 - Franky

Aimee's Blog

秒杀怎么防机器人:验证码之外的几道防线 防薅羊毛怎么做:号码风控、设备指纹与分层策略 接口限流怎么选:固定窗口、滑动窗口、令牌桶 批量打包下载怎么设计:流式 ZIP 与异步任务 图片处理怎么做:异步处理与 CDN 实时参数 报表可重跑的设计:快照、水位线与幂等 UV 统计为什么有误差:精确计数与近似计数怎么选 大数据量导出怎么做:流式写入与异步任务 排行榜的两类需求,难度差了一个数量级 内容审核怎么选:先审后发,还是先发后审 实时热榜怎么设计:Redis ZSet 与热度分值 互关状态怎么保持同步:双向关系的维护与缓存失效 并发点赞的计数设计:原子操作与最终对账 删了父评论,子评论怎么处理——先想清楚用户看到的是什么 定时消息任务触发了两次 消息量大,写扩散还是读扩散——接到这个需求先问清楚规模 同一条消息推送了三次 活动结束了,用户还在收短信 接到注销需求,先问两个问题 微服务与服务拆分:何时拆、怎么拆 异步与事件驱动架构:把协作从「打电话」改成「发消息」 高可用设计:怎么让系统尽量不宕机 可扩展性设计:怎么让系统加机器就能扛更多 缓存架构:多级缓存怎么搭 高并发三板斧:限流、熔断、降级 架构设计到底在设计什么 —— 从单体到微服务的演进 服务成本账:一个服务一个月烧多少钱 可观测性:线上出问题怎么查 API 设计:好接口长什么样 消息队列:为什么要 MQ,以及丢失、重复、顺序怎么破
大文件上传怎么设计:分片、断点续传、秒传
Aimee · 2026-08-14 · via Aimee's Blog

大文件上传怎么设计:分片、断点续传、秒传

大文件上传(比如 2GB 的视频),浏览器直接整包 POST 的结果几乎注定:传到一半超时断掉,从头再来,再断。

做大文件上传之前,要明确三件事:分片、断点续传、秒传。三件事不是一件事,实现复杂度逐级递增。


分片上传

把大文件切成固定大小(如 5MB)的片,逐片上传,全部传完后通知后端合并:

前端

const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
const chunks = [];
for (let start = 0; start < file.size; start += CHUNK_SIZE) {
    chunks.push(file.slice(start, start + CHUNK_SIZE));
}
// 并发上传所有分片(控制并发数,如同时传 3 片)
await Promise.all(chunks.map((chunk, i) => uploadChunk(uploadId, i, chunk)));
// 全部完成后通知合并
await mergeChunks(uploadId, chunks.length);

后端接收分片,按 uploadId + chunkIndex 存储,收到 merge 请求后顺序拼接:

public void mergeChunks(String uploadId, int totalChunks) throws IOException {
    Path targetFile = Paths.get(UPLOAD_DIR, uploadId, "merged");
    try (OutputStream out = Files.newOutputStream(targetFile)) {
        for (int i = 0; i < totalChunks; i++) {
            Path chunkFile = Paths.get(UPLOAD_DIR, uploadId, "chunk_" + i);
            Files.copy(chunkFile, out);
        }
    }
}

断点续传

用户传到第 6 片断网了,重新上传时不要从头开始。

服务端记录哪些分片已经上传成功:

// 客户端上传前先查:哪些分片已存在
public List<Integer> getUploadedChunks(String uploadId) {
    return chunkDao.getUploadedIndexes(uploadId);
}

前端拿到已上传列表后,跳过这些分片,只传缺失的。

uploadId 由服务端在开始上传前颁发(POST /upload/init),客户端将其持久化(localStorage),断线重连后用同一个 uploadId 继续。


秒传

文件 MD5 相同,说明内容完全一致——服务端已有这个文件,不需要再传一遍:

// 上传前先检查 MD5
public UploadCheckResult checkMd5(String md5) {
    FileRecord existing = fileDao.findByMd5(md5);
    if (existing != null) {
        return UploadCheckResult.instant(existing.getUrl()); // 秒传成功,直接返回 URL
    }
    String uploadId = generateUploadId();
    uploadSessionDao.create(uploadId, md5);
    return UploadCheckResult.needUpload(uploadId);
}

秒传的前提是服务端有中心化存储,文件去重在存储层实现(同一 MD5 只存一份,多个用户引用同一个 object key)。


用 OSS 原生分片上传,不要自己写合并

上面的合并逻辑自己实现,需要磁盘 I/O、需要管理临时文件清理,还有合并失败的容错处理——很麻烦。

阿里云 OSS / 腾讯云 COS / AWS S3 都有原生的分片上传 API:

  1. InitiateMultipartUpload → 得到 uploadId
  2. 客户端直传每个分片到 OSS(UploadPart),带 uploadId + partNumber
  3. 所有分片传完,调 CompleteMultipartUpload——OSS 自己合并

客户端直传 OSS,不走你的服务器,节省带宽和服务器内存。服务端只需要给前端颁发有时效的 STS 临时凭证用于直传授权。


三件能力——分片、断点续传、秒传——按需选,不是每个场景都要全上。文件小(< 100MB)只需要分片;只有用户会频繁上传重复文件才值得做秒传。复杂度由需求决定,不是越全越好。