跨集合迁移必须用session.startTransaction()开启事务,否则操作各自提交无原子性;仅支持副本集或分片集群,需正确配置连接串、显式设置readConcern:"snapshot"和writeConcern:"majority",并分批控制事务大小。

事务中跨集合迁移必须用 session.startTransaction()
不显式开启事务,insertOne() 和 deleteOne() 会各自提交,根本谈不上原子性。MongoDB 的事务只在 session 级别生效,且仅支持副本集或分片集群(单节点不支持)。常见错误是直接在普通连接上调用事务方法,结果抛出 MongoServerError: Transaction numbers are only allowed on a replica set member or mongos。
- 确保连接字符串指向副本集(含
?replicaSet=rs0)或 mongos - 调用
client.startSession()获取 session 实例,再对 session 调用startTransaction() - 所有读写操作(包括
find()、updateOne()、deleteMany())都必须传入该 session - 事务内不能使用不支持的操作,例如
$out、$merge聚合阶段,也不能跨数据库操作(除非是同一个逻辑数据库)
迁移逻辑必须封装在 try/catch + session.commitTransaction() / session.abortTransaction()
事务不会自动回滚:哪怕中间某步抛异常,session 也不会感知并终止事务,必须手动 abortTransaction(),否则会卡在 open 状态,占用资源并可能阻塞后续操作。实际开发中常漏掉 catch 块里的 abort,或在 finally 中未判断事务状态就强行 commit。
- 务必在
try块末尾调用session.commitTransaction() -
catch块中先检查session.inTransaction(),为 true 再调用abortTransaction() - 不要在
finally中无条件 commit/abort;事务已提交或中止后再次调用会报MongoServerError: No transaction started - 示例关键片段:
try { await sourceColl.deleteOne({ _id: doc._id }, { session }); await targetColl.insertOne(doc, { session }); await session.commitTransaction(); } catch (error) { if (session.inTransaction()) await session.abortTransaction(); throw error; }
注意 readConcern 和 writeConcern 对事务可见性的影响
默认 readConcern: "local" 在事务中可能导致读到未提交数据(其他事务不可见),但自己事务内读写一致;而 readConcern: "snapshot" 才能保证事务内多次读取看到相同快照——这对迁移中“先查再删再插”的逻辑很关键。同时,writeConcern 设置过松(如 { w: 1 })可能让写入在主节点落盘后就返回,但未同步到多数节点,事务提交后若主节点宕机,可能丢失数据。
- 推荐显式设置
readConcern: { level: "snapshot" }和writeConcern: { w: "majority" } - 这两个选项需在
startTransaction()时传入,不是在单个操作里设:session.startTransaction({ readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } }); - 如果集合启用了文档验证(validation),插入前需确认
doc符合 schema,否则insertOne()报错导致事务中止
批量迁移要控制事务大小,避免超时或内存溢出
MongoDB 事务有默认 60 秒超时(maxTransactionTimeMS),且整个事务的 oplog 条目不能超过 16MB。一次迁移上万条文档极易触发 MongoExpiredTransactionError 或 MongoWriteConcernError。别指望靠调大超时解决——长事务会阻塞复制延迟、增加锁竞争,还可能拖垮整个副本集。
- 按
_id或时间范围分批,每批 ≤ 1000 条(视文档平均大小调整) - 每批单独开一个事务,失败则重试该批,不要全量回滚
- 用
session.withTransaction()替代手写 try/catch 更稳妥(它内置了自动 abort 和重试逻辑) - 迁移前在 source 集合建好
{ _id: 1 }索引,避免deleteOne()全表扫描
事务跨集合迁移真正难的不是语法,而是对 session 生命周期、错误传播路径和底层复制机制的理解。最容易被忽略的是:事务提交后,应用层仍需处理网络分区导致的“commit 成功但响应丢失”问题——此时应设计幂等回查逻辑,而不是盲目重试。











