$merge 阶段本身不触发 WriteConflict(错误码 112),但其最终写操作在并发修改同一文档时,因 WiredTiger 版本检测失败而抛出该错误;常见诱因包括 on 字段无索引、高频更新同 key 文档、聚合输出重复匹配等。

$merge 阶段本身不会触发 WriteConflict(错误码 112)——它不是事务内操作,也不参与 WiredTiger 的多文档事务锁机制。你在 $merge 上遇到的“写冲突”,实际是底层执行时与其他并发写入(尤其是事务或高并发 update)争夺同一文档/索引页导致的,常见于聚合结果与目标集合存在重叠更新路径的场景。
为什么 $merge 会报 WriteConflict?
虽然 $merge 是原子性聚合阶段,但它最终仍要对目标集合执行写操作(如 upsert、replace 或自定义 pipeline)。当它尝试修改的文档正被另一个事务(或非事务写入)同时修改时,WiredTiger 在提交物理写入前检测到版本不一致,就会中止当前操作并抛出 MongoError,其中 error.code === 112。
- 典型诱因:$merge 的
on字段未建索引,导致扫描扩大、锁住更多文档 - 目标集合正在被大量
findOneAndUpdate并发更新(尤其用相同_id或业务主键) - 聚合管道输出大量匹配同一
on值的文档(例如误用$group后未去重,再 $merge 到单 key) - 在副本集上运行 $merge 时,主节点负载已高,写入排队加剧竞争
如何安全使用 $merge 避免 WriteConflict
关键不是“重试 $merge”,而是让它的写行为更轻量、更确定、更隔离:
- 确保
on字段(或字段组合)在目标集合上有**唯一索引**或至少是**高效查询索引**;缺失索引会让 $merge 扫描全表,极大提高冲突概率 - 避免在 $merge 的
whenMatched中使用复杂 pipeline;优先选"replace"或"keepExisting",它们语义明确、锁粒度小;若必须用 pipeline,确保其中不包含跨文档读取(如$lookup)或二次写入 - 不要把 $merge 当作“批量 upsert 替代品”滥用;如果只是插入/更新少量已知 key 的文档,直接用
bulkWrite([{updateOne: {...}}, ...])更可控、更快、不触发事务层逻辑 - 若必须高频调用 $merge(如物化视图刷新),将其拆分为小批次(例如每次最多 1000 条),并在批次间加
await new Promise(r => setTimeout(r, Math.random() * 5))随机退避
遇到 WriteConflict 错误时怎么处理
不能像事务那样用 session.withTransaction() 包裹 $merge ——它根本不走事务路径。你捕获到 error.code === 112 时,说明写已失败,必须重建整个聚合管道并重试:
- 重试逻辑必须在外层应用代码中实现(例如 Node.js 的
while (retries--) { try { ... } catch (e) { if (e?.code === 112) continue; throw e; } }) - 每次重试前必须重新生成聚合管道(尤其涉及时间戳、计数器等动态值),避免 pipeline 内部状态污染
- 禁止复用上一次的游标或 aggregation cursor;$merge 必须从头跑完管道
- 重试上限建议设为 3 次;超过说明设计有问题——比如目标集合正被其他服务高频覆盖,应协调写入节奏或引入中间缓冲层
真正容易被忽略的是:$merge 的冲突往往暴露的是架构级问题,而非语法错误。比如用它实时合并用户行为流到 profile 集合,却没意识到所有行为都落向同一个 userId 文档——这本质是单点写热点,再怎么优化 $merge 也治标不治本。这时候该换思路:用时间分片(profile_202609)、预聚合(先按小时汇总再 merge)、或彻底弃用 $merge 改用 change stream + 异步 worker。


















