事务中能用 bulkWrite() 但不推荐,因其默认不绑定会话易导致“伪事务”;真正安全的批量写入需显式传入同一 IClientSession,且须拆批(如1000–5000文档/批)并每批独立启停事务以避免 WriteConflict 和缓存打满。

事务中不能直接用 bulkWrite()?
不是不能,而是不推荐。MongoDB 的 bulkWrite() 本身不支持在事务上下文中调用(驱动层会报错或静默降级),尤其在 Java、C#、Rust 等强类型驱动中,bulkWrite() 默认运行在无会话模式下。如果你传入了事务会话但没显式绑定,操作会脱离事务边界,变成“伪事务”——看起来在事务里,实际写入已提交。
真正能进事务的批量写入,必须满足两个条件:所有操作都通过同一个 IClientSession(或等价会话对象)执行,且每个操作都明确指定该会话。例如 Java 驱动中要用 collection.insertMany(docs, new InsertManyOptions().session(session)),而不是裸调 insertMany()。
insertMany() 在事务里为什么还报 WriteConflict?
根本原因不是代码写错了,而是事务生命周期和 WiredTiger 缓存不匹配。当单次 insertMany() 批量过大(比如 10 万+ 文档)、又塞进一个长事务时,WiredTiger 要为整个事务维护快照、预写日志和脏页缓存。默认缓存大小(物理内存的 50%)在高吞吐场景下极易打满,触发页面争用,最终抛出 WriteConflict ——这不是业务冲突,是存储引擎说“我缓存不够,得重试”。
- 事务持续时间超过 60 秒,风险陡增
- 单批文档总大小超过 100 MB,WiredTiger 压力明显上升
- 集群部署在 Atlas 上?
wiredTigerCacheSizeGB参数不可调,只能靠拆批 + 降事务粒度来缓解
怎么拆批才不丢数据、不重复?
关键在循环边界和清空逻辑。常见错误是用 i == list.size() 判断最后一组,但 list.size() 是固定值,导致最后一组永远不触发提交;或者 clear() 后没重置引用,造成 batch 对象被后续循环污染。
安全做法是把批处理和事务完全解耦:
- 每批独立开启/提交事务,比如每次
insertMany()包一层session.startTransaction()→insertMany(..., session)→session.commitTransaction() - 批大小设为 1000–5000,兼顾网络包大小(
maxWriteBatchSize默认 100000)和事务内存开销 - 循环用索引分片:for (int i = 0; i batch = docs.subList(i, Math.min(i + batchSize, docs.size())); ... }
非必要别套事务
如果只是单集合插入、无跨文档一致性要求(比如不需要“这批全成功或全失败”),直接去掉事务封装是最简单有效的解法。实测显示,移除事务后吞吐可提升 3–5 倍,WriteConflict 彻底消失。
事务的代价是真实存在的:锁持有时间延长、快照内存占用翻倍、WAL 日志膨胀。除非你在做银行转账类多文档联动更新,否则大部分 ETL 或日志导入场景,insertMany() 直接上更稳。
真正容易被忽略的是:事务不是“加个壳就安全”,它把问题从应用层推给了存储引擎。缓存不够、超时设置不合理、批大小失衡,任何一个点没对齐,都会在高负载时突然崩掉。

















