监听 fs.files 的 insert 事件可行但需满足三前提:mongodb 6.0+ 副本集、连接含 ?replicaset=xxx 且拓扑健康、用户具目标库 read 权限;须加 $match 过滤仅 insert;读取时用 majority readconcern 并校验文件非空、非临时;resumetoken 必须正确序列化持久化。

只监听 fs.files 是可行的,但必须满足三个硬性前提
监听 fs.files 集合的 insert 事件,确实是目前最接近“文件上传完成”语义的方案,但它不是开箱即用的安全操作。你必须确认以下三点全部成立,否则会漏事件、读空文件,甚至同步失败:
- MongoDB 是 6.0+ 副本集(单机或 Serverless 实例直接不支持
watch()) - 连接字符串中明确包含
?replicaSet=xxx,且驱动能识别拓扑(mongosh里rs.status()要显示健康节点) - 执行
watch()的数据库用户拥有目标库的read权限(仅changeStream权限不够,底层依赖 oplog 读取)
collection.watch() 必须加聚合管道过滤 operationType
裸调用 db.collection("fs.files").watch() 会收到所有变更类型(包括 delete、update、invalidate),而 GridFS 同步只关心新文件就绪。不加过滤会导致误触发或逻辑混乱:
- 必须用
[{ $match: { operationType: "insert" } }]显式限定,避免处理replace或update事件(GridFS 不走这些路径) - 不要在管道里加
$lookup或复杂计算——Change Streams 的聚合阶段能力受限,$match是最安全的选择 - 如果用 Node.js 驱动,记得传
{ fullDocument: "required" },确保每次都能拿到完整的fs.files文档(含_id、length、chunkSize)
收到事件后不能立刻调用 openDownloadStream()
Change Stream 事件到达时,fs.chunks 数据可能尚未在当前节点完成复制,直接下载会抛 GridFSFileNotFoundError 或返回截断流。这不是代码 bug,而是副本集最终一致性导致的可见性延迟:
- 必须用
readConcern: { level: "majority" }显式指定读取级别,强制等待 majority 节点写入完成 - 给
openDownloadStream()加{ timeoutMS: 10000 },防止因锁或网络卡住整个同步流程 - 跳过
length === 0的文档(空文件)、filename以.tmp结尾的文档(常见于客户端重试中间态) - 绝对不要手动
find()fs.chunks并拼接二进制——顺序和分片逻辑由 GridFS 驱动封装,自行实现极易出错
resumeToken 持久化是同步可靠性的唯一防线
Change Streams 不保存消费位点,断连后靠 resumeAfter 恢复。但 resumeToken 是 BSON 类型 primitive.D,不是字符串或 JSON 可序列化的结构。存错格式等于放弃幂等性:
- Go 用户:用
token.MarshalBSON()存二进制,恢复时用primitive.UnmarshalBSON() - Node.js 用户:用
JSON.stringify(token)会丢失精度,应存token._data.buffer的 base64 编码 - 每次成功完成一次文件下载 + 上传到云存储后,再更新持久化存储里的 token;失败时不更新,下次重试
- 本地内存缓存 token 是高危操作——进程重启即丢,必须落盘或写 Redis / 专用集合
真正难的不是监听本身,而是把 fs.files.insert 这个瞬间,和“此刻所有 chunk 已就绪、可被安全读取”这个状态对齐。副本集延迟、驱动行为差异、权限配置粒度,任何一个环节松动,都会让同步变成间歇性故障。











