GridFS上传不保证原子性,因fs.files和fs.chunks写入独立且非事务性;强一致场景需用4.0+事务手动写入两集合,否则推荐默认上传+定期校验修复。

GridFS 上传本身不保证原子性
GridFS 是 MongoDB 提供的文件存储方案,但它底层只是把大文件切块存进 fs.chunks 和元信息存进 fs.files 两个集合——这两个写入是独立的、非事务性的。哪怕你用的是 4.0+ 支持多文档事务的版本,GridFSBucket 的 uploadFromStream 方法默认也不在事务里执行。
常见错误现象:fs.files 里能看到一条记录,但对应的所有 fs.chunks 找不到或不全;或者反过来,一堆 chunk 存了,但 fs.files 没写入,变成“孤儿块”。
- 上传中途网络断开、进程崩溃、MongoDB 写入失败,都会导致这种不一致
- 如果你依赖
_id做后续查询,而fs.files写失败了,那这个_id就永远查不到文件 - 手动用
insertOne先写fs.files再写fs.chunks更危险——没有锁、没有回滚,失败后清理成本极高
用 GridFSBucket + 显式事务兜底(4.0+)
唯一可控的原子性路径,是绕过默认上传逻辑,自己构造事务:先生成唯一 _id,再在事务中同步写 fs.files 和所有 fs.chunks 文档。但注意:Node.js 驱动的 GridFSBucket 不暴露 chunk 写入接口,所以得退回到原生 collection 操作。
使用场景:你有强一致性要求(比如金融凭证、合同扫描件),且 MongoDB 版本 ≥ 4.0,副本集 / 分片集群已启用事务支持。
- 必须用
withTransaction包裹整个写入流程,不能只包fs.files.insertOne - chunk 数据需提前分片(默认 255KB/块),用
Buffer切好,再逐个insertOne到fs.chunks -
fs.chunks文档必须含files_id(指向fs.files._id)、n(序号)、data(BinData类型)字段,缺一不可 - 事务内不要做任何外部 I/O(如读磁盘、调 HTTP),否则可能超时回滚失败
示例关键片段:
const session = client.startSession();
await session.withTransaction(async () => {
const fileId = new ObjectId();
await db.collection('fs.files').insertOne({
_id: fileId,
filename: 'report.pdf',
length: 1234567,
chunkSize: 255 * 1024,
uploadDate: new Date(),
md5: '...'
}, { session });
<p>const chunks = splitIntoChunks(buffer, 255 * 1024);
for (let i = 0; i </p>
更现实的选择:接受最终一致性 + 后置校验
绝大多数业务其实不需要严格原子性,而是需要“能快速发现并修复不一致”。MongoDB 官方也建议:优先用 GridFSBucket 默认上传,再配定期校验任务。
性能影响很小,兼容性最好(3.6 也能用),且避免事务带来的锁竞争和超时风险。
- 上传后立即用
bucket.find({ _id: fileId })查一次,确认length和实际 chunk 数匹配(db.fs.chunks.countDocuments({ files_id: fileId })) - 把校验逻辑封装成小脚本,每小时扫一遍最近 1 小时的
fs.files,告警不一致项 - 补救方式简单:删掉残缺的
fs.files记录 + 对应所有fs.chunks,重传即可 - 如果担心重传覆盖,上传前先用
bucket.find({ filename: 'x.pdf', metadata.version: 2 })加业务维度去重
别踩这些坑
很多问题不是出在 GridFS 本身,而是周边设计没对齐。
- 别在事务里用
GridFSBucket.uploadFromStream——它内部会新开 session,直接报错Transaction numbers are not supported - 别用
ObjectId()作为业务主键再传给 GridFS——万一上传失败,这个 ID 就浪费了,且无法被业务层感知 - 分片集群下,
fs.files和fs.chunks必须用相同 shard key(通常是_id),否则事务跨分片会失败 - 驱动版本太老(如 node-mongodb-native fs.chunks,会静默降级为普通写入
真正麻烦的从来不是“怎么写进数据库”,而是“怎么定义‘上传成功’——是客户端收到 200?还是 fs.files 可查?还是所有 chunk MD5 校验通过?”这个问题没想清楚,后面所有技术方案都会偏航。











