findAndModify及其别名在MongoDB事务中被服务端硬性禁止,因与两阶段提交冲突;正确做法是在同一事务会话中用snapshot读取→应用层判断→updateOne写入。

findAndModify 在事务中根本不能用
MongoDB 从 4.0 开始支持多文档事务,但 findAndModify(以及它的别名 findOneAndUpdate、findOneAndDelete 等)在事务中是明确被禁止的。如果你在 session.startTransaction() 后调用它,会直接报错:Command findAndModify is not supported in transactions。
这不是驱动或语法问题,而是服务端硬性限制 —— 因为 findAndModify 内部依赖 snapshot 隔离和 write concern 的特殊协调机制,与事务的两阶段提交(2PC)模型冲突。
所以别试了,也别找“绕过方法”,官方不支持就是不支持。
事务内做条件写入的正确姿势:先查再判再写
要在事务里实现“满足某条件才更新”的逻辑,必须拆成显式的三步:读取 → 判断 → 写入。关键点在于:所有操作必须在同一个 session 中,且读操作要用 readConcern: "snapshot"(事务默认启用),确保读到的是事务快照内的数据,不会被并发修改干扰。
- 用
collection.findOne({ _id: ... })获取当前文档,不要用find().toArray()或带排序/跳过的查询(事务中部分聚合阶段也不支持) - 在应用层判断业务条件(比如
doc.status === "pending"),**不能把条件下推到 update 的$set里假装原子** - 确认通过后,调用
collection.updateOne()或collection.replaceOne(),update 操作本身是事务内原子的
示例(Node.js + MongoDB driver):
const session = client.startSession();
try {
await session.withTransaction(async () => {
const doc = await collection.findOne({ _id: id }, { session });
if (!doc || doc.status !== "pending") {
throw new Error("condition not met");
}
await collection.updateOne(
{ _id: id },
{ $set: { status: "processed", processedAt: new Date() } },
{ session }
);
});
} finally {
await session.endSession();
}
为什么不用 upsert + 条件字段模拟?
有人想用 updateOne({ _id, status: "pending" }, { $set: { ... } }, { upsert: true }) 替代,认为“匹配不到就不更新”,但这在事务中依然有风险:
- 如果并发事务同时读到
status: "pending",又都执行这个 update,第二个会变成 upsert —— 插入一个重复_id的文档,触发唯一键错误,但此时第一个事务可能已提交,状态已变,逻辑就乱了 - 更糟的是,upsert 在事务中创建新文档时,
_id若是自动生成(如 ObjectId),会导致事务日志膨胀,且无法回滚“已插入但未提交”的中间状态 -
updateOne的 filter 是事务快照内可见的,但 upsert 的“不存在判断”行为在高并发下不如显式findOne+ 条件判断可控
性能和边界要注意什么
事务不是银弹,尤其涉及条件写入时:
- 事务生命周期越长,锁持有时间越久,容易触发
maxTransactionLockRequestTimeoutMillis(默认 5 分钟)超时,所以判断和写入之间别加延迟、日志或网络请求 - 读操作必须命中索引,否则事务内全表扫描会严重拖慢并增加锁粒度;确保
{ _id }或你用来查的字段建了索引 - 事务不支持跨分片写入(sharded cluster 中,所有操作必须落在同一分片上),
_id是分片键或使用shard key查询才能保证路由一致 - driver 版本要 ≥ 4.0(对应 MongoDB Server 4.0+),老版本
session对象没有withTransaction方法,得手动调startTransaction/commitTransaction
真正需要原子条件写入的场景,往往意味着业务强一致性要求高——这时候宁可拆成明确的读-判-写,也别幻想靠单条命令糊弄过去。MongoDB 的事务语义清晰,但边界也很直白:它只保证“这些操作要么全成功,要么全失败”,不帮你隐藏并发逻辑。











