GridFS重复块源于客户端未幂等写入、uploadId设计不当、驱动自动重试及fs.files提前插入;须严格校验chunk唯一性、全程统一uploadId、禁用提前写fs.files,并在chunk写入前查询去重。

fs.chunks 重复块的根源是客户端未做幂等写入
GridFS 不校验 chunk 是否已存在,也不阻止重复插入相同 { files_id, n } 的文档。只要客户端发来 insertOne 请求,服务端就照单全收——哪怕 files_id 和 n 完全一样。这不是 bug,而是设计使然:GridFS 把“去重”这件事完全交给了上层逻辑。
常见触发场景包括:网络超时后前端重试、页面刷新导致上传逻辑重启、多线程并发写同一文件但未同步序号、或断点续传时未查库直接重发 chunk。
- 每次写 chunk 前必须先执行
fs.chunks.countDocuments({ files_id: uploadId, n: targetN }),结果 > 0 就跳过 - 不能只查
files_id—— 这会漏掉单个 chunk 重复,而放行其他 chunk - 不要用
findOneAndUpdate(..., { upsert: true })代替查询+条件插入:它无法保证原子性写入(尤其在分片集群),且不返回是否真插入了新文档
uploadId 设计不当导致跨任务污染
如果多个上传任务共用同一个 files_id(比如都用时间戳或随机字符串当 _id),一旦两个任务恰好生成相同值,它们的 chunk 就会混进同一个 files_id 下,后续按 n 拼接时必然错乱,甚至出现重复 n 值。
更隐蔽的问题是:前端用 Math.random() 生成 uploadId,Node.js 后端又用 new ObjectId() 覆盖——两边不一致,导致客户端认为“我传的是 A”,服务端存的是 B,幂等查询永远查不到自己写的 chunk。
-
uploadId必须全程一致:从浏览器生成、传参、到openUploadStream()的options.id,再到所有 chunk 的files_id字段 - 推荐用 UUID v4(如
crypto.randomUUID()),避免时间戳/计数器类弱熵 ID - 服务端收到 uploadId 后,应校验其格式合法性(例如是否为合法 UUID),非法则拒绝,防止被恶意构造
驱动层自动重试机制放大重复风险
MongoDB Node.js 驱动(6.x+)默认开启 retryWrites: true,当网络抖动导致写请求超时,驱动会在内部自动重发——但你代码里只调了一次 insertOne,底层却可能发了两次,且第二次不会带任何幂等标识。
这个行为在 fs.chunks 上尤为危险:因为 chunk 写入是无状态的,重试请求和原始请求内容完全一样,服务端无法区分。
- 关闭自动重试不是好办法(影响其他操作可靠性),正确做法是在写 chunk 前加幂等判断,覆盖重试场景
- 不要依赖
result.upsertedCount === 1判断是否成功写入——重试成功时它仍是 1,但实际已存在 - 若必须用批量写(如
insertMany),务必设ordered: false,并逐个检查result.insertedIds或result.getUpsertedIds(),跳过已存在的项
fs.files 提前插入导致 chunk 查询逻辑失效
有些实现为了“快速反馈”,在 chunk 还没写完时就先插入一条 fs.files 记录(哪怕 length 是 0)。这会导致后续上传任务查询 fs.files 时误判文件已存在,转而尝试复用该 _id 写 chunk,结果新旧两批 chunk 全堆进同一个 files_id 下。
更糟的是,如果旧任务残留了部分 chunk,新任务又从 n=0 开始写,就会覆盖或并存——最终拼出来的文件要么截断、要么错位、要么重复。
-
fs.files文档必须严格在全部 chunk 写完、且fs.chunks.countDocuments({ files_id }) === expectedTotal校验通过后,才插入 - 插入
fs.files时,字段必须完整:_id(即 uploadId)、length(真实字节数)、chunkSize(与客户端切片一致)、uploadDate(ISODate)、filename - 不要用
bucket.find()查fs.files是否存在来决定是否跳过上传——它只反映元数据是否存在,不反映 chunk 完整性


















