基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

如何实现支持断点续传和秒传的大文件上传服务?

知识点图片

实现支持断点续传(Resumable Upload)和秒传(Instant Upload / Deduplication)的大文件上传服务,核心思想是将一个大文件切片(Chunking),并基于文件内容计算唯一标识(Hash)

以下是完整的技术方案设计与实现步骤:


核心流程图解

  1. 计算 Hash:前端读取文件内容,计算唯一的 MD5 值(作为文件的指纹)。
  2. 秒传检查:前端将 Hash 发送给服务端,询问“这个文件你有了吗?”
    • → 服务端直接返回成功(秒传完成)。
    • → 进入下一步。
    • 部分有 → 服务端返回已上传的切片列表(用于断点续传)。
  3. 文件切片:前端将文件按固定大小(如 5MB)切割成多个块。
  4. 并发上传:过滤掉服务端已有的切片,将剩余切片上传。
  5. 合并请求:所有切片上传完成后,前端发送“合并”指令,服务端将切片还原为文件。

一、 前端实现要点

1. 文件切片 (File Slicing)

利用 HTML5 的 File.prototype.slice 方法。File 对象继承自 Blob,可以直接切割。

javascript
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB

function createFileChunks(file, size = CHUNK_SIZE) {
  const chunks = [];
  let cur = 0;
  while (cur < file.size) {
    chunks.push({ index: cur, file: file.slice(cur, cur + size) });
    cur += size;
  }
  return chunks;
}

2. 计算文件 Hash (Web Worker + SparkMD5)

对于大文件,在主线程计算 Hash 会导致页面卡顿。必须使用 Web Worker 在后台线程计算,并配合 spark-md5 库进行增量计算。

javascript
// hash.worker.js (伪代码)
self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;
  
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result); // 增量计算
      if (count === fileChunkList.length) {
        self.postMessage({ hash: spark.end() }); // 完成
      } else {
        loadNext(count);
      }
    };
  };
  loadNext(0);
};

3. 上传控制逻辑

前端需要维护一个请求队列,控制并发数(如最多同时上传 4 个切片),并处理网络错误重试。

  • 检查接口POST /verify,参数 { fileHash, fileName }。返回 { shouldUpload, uploadedList }
  • 上传接口POST /upload,使用 FormData 传输切片文件、Hash、切片索引。

二、 后端实现要点 (以 Node.js/Java/Go 为例)

后端主要负责:接收切片、临时存储、合并切片、记录文件元数据。

1. 数据库设计

需要一张表来记录文件的状态,用于秒传判断。

字段名 类型 说明
id int 主键
file_hash string 文件 MD5 (唯一索引)
file_path string 真实存储路径 (OSS地址或本地路径)
status int 0:上传中, 1:已完成
created_at datetime 创建时间

2. 接口设计

A. 验证接口 (/verify)

  1. 根据 fileHash 查询数据库。
  2. 如果记录存在且状态为“完成”,直接返回 shouldUpload: false (秒传成功)。
  3. 如果记录不存在或状态为“上传中”,检查服务端临时目录(如 /temp/{fileHash}/),读取已存在的所有切片索引,返回 uploadedList: [0, 1, 5...]

B. 切片上传接口 (/upload)

  1. 接收 chunk, hash, index
  2. 将切片存储在临时目录:/uploads/temp/{hash}/{index}
  3. 注意:不需要每次上传都写数据库,直接利用文件系统判断切片是否存在即可。

C. 合并接口 (/merge)

  1. 接收 fileHash, fileName, chunkSize
  2. 找到 /uploads/temp/{hash}/ 目录。
  3. 读取目录下所有切片,按索引排序(1, 2, 3...)。
  4. 流式读写:创建一个可写流指向最终文件路径,依次将切片读入并管道(pipe)写入,防止内存溢出。
  5. 写入完成后,删除临时目录及切片。
  6. 在数据库中写入/更新该文件的记录。

3. 合并代码逻辑示例 (Node.js)

javascript
const mergeFileChunk = async (filePath, fileHash, size) => {
  const chunkDir = path.resolve(UPLOAD_DIR, fileHash);
  const chunkPaths = await fse.readdir(chunkDir);
  
  // 根据切片下标排序,否则合并后文件损坏
  chunkPaths.sort((a, b) => a.split('-')[1] - b.split('-')[1]);
  
  await Promise.all(
    chunkPaths.map((chunkPath, index) =>
      pipeStream(
        path.resolve(chunkDir, chunkPath),
        // 指定位置写入,保证并发合并时的顺序(如果使用并发写入的话)
        // 或者简单地串行 append
        fse.createWriteStream(filePath, {
          start: index * size,
          end: (index + 1) * size
        })
      )
    )
  );
  // 删除切片目录
  fse.rmdirSync(chunkDir);
};

三、 关键问题与优化方案

1. 计算 Hash 太慢怎么办?(抽样 Hash)

对于几 GB 的视频,全量计算 MD5 即使在 Worker 中也需要几十秒。
优化方案:抽样计算 (Sample Hash)

  • 取文件头 2MB。
  • 取文件尾 2MB。
  • 中间每隔 20MB 取前 2KB。
  • 将这些片段拼接后计算 MD5。
  • 代价:极小概率的 Hash 碰撞(不同文件被误判为相同),但在非金融级场景通常可接受。

2. 断点续传的清理机制 (Garbage Collection)

如果用户上传了一半就消失了,临时目录会堆积大量切片。
解决方案

  • 设置 Cron Job(定时任务),每天扫描临时目录。
  • 如果某个文件夹的修改时间超过 24 小时,视为过期,直接删除。

3. 并发合并时的 IO 压力

如果多个大文件同时请求合并,服务器 IO 会瞬间飙升。
解决方案

  • 使用消息队列(RabbitMQ / Kafka)处理合并任务,削峰填谷。
  • 上传完成后,前端只收到“已加入合并队列”的响应,通过 WebSocket 或轮询查询最终状态。

4. 存储选择

  • 本地磁盘:适合小规模,需处理分布式部署时的文件共享问题(通常需要挂载 NFS 或使用粘性 Session,但不推荐)。
  • 对象存储 (AWS S3 / 阿里云 OSS):推荐方案。
    • 利用 OSS 提供的 Multipart Upload API
    • 后端只负责签发“上传凭证”,前端直接把切片上传到 OSS,最后调用 OSS 的合并接口。这样可以节省应用服务器的带宽和 CPU。

5. 安全性

  • 文件类型检查:不要相信前端传来的 Content-Type 或扩展名。在合并完成后,读取文件头(Magic Number)判断真实文件类型。
  • 恶意覆盖:不同用户上传相同 Hash 的文件(秒传),A 用户删除了文件,不应影响 B 用户。数据库设计应为 UserFile 关联表,物理文件只有一份,引用计数为 0 时才物理删除。

总结

实现一套健壮的大文件上传系统,核心在于:

  1. 前端:Web Worker 计算 Hash + 切片 + 并发控制。
  2. 后端:检查 Hash (秒传) + 接收切片 + 流式合并。
  3. 架构:利用对象存储的分片上传能力是生产环境的最优解。
00:00
00:00