mongodb 没有真正的 select for update,因其设计避免悲观锁,仅写操作在 wiredtiger 下自动加文档级 x 锁;需用事务内 findoneandupdate() 或带条件 updateone() 模拟原子性,且依赖副本集、wiredtiger 引擎及兼容版本。

不能直接实现 SELECT FOR UPDATE 语义,但可通过事务 + findOneAndUpdate() 在 WiredTiger 引擎下达成等效效果——前提是操作单文档、使用副本集部署、且逻辑允许“读-改”原子化。
为什么 MongoDB 没有真正的 SELECT FOR UPDATE
MongoDB 的设计哲学是避免阻塞式悲观锁。它不支持在事务外对文档加排他读锁,find() 即使在事务中也不会触发写锁或阻塞其他会话;只有写操作(如 updateOne()、findOneAndUpdate())才会由 WiredTiger 自动加文档级 X 锁。这意味着:
-
db.collection.find({ _id: ... }).forEach(...)+ 后续updateOne()必然存在竞态窗口,不是锁的问题,而是缺少原子性 - 事务内单独执行
find()不会锁定文档,其他事务仍可并发修改该文档 -
SELECT FOR UPDATE是 MySQL InnoDB 的行锁原语,依赖 B+ 树索引和 gap lock,MongoDB 的 BSON 文档模型与之底层机制完全不同
用 findOneAndUpdate() 在事务中模拟行锁效果
当业务必须基于当前文档状态做判断(比如读出 price 和 discount 后计算 finalAmount),又要求后续更新不被并发干扰,唯一可靠方式是把读和写合并为一次原子操作,并包裹在事务中:
session.startTransaction();
try {
const doc = collection.findOneAndUpdate(
{ _id: ObjectId("..."), status: "active" },
{ $set: { finalAmount: { $multiply: ["$price", { $subtract: [1, "$discount"] }] } } },
{ returnDocument: "after", session }
);
// doc.value 包含更新后的完整文档,且该文档在事务提交前已被 WiredTiger 加 X 锁
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
}
关键点:
- 必须传入
session参数,否则不参与事务 - 查询条件(如
status: "active")要足够精确,避免匹配多文档导致事务升级为多文档锁(开销剧增) -
returnDocument: "after"确保你能拿到最新值用于后续逻辑,但注意:该值仅对当前事务可见 - WiredTiger 对匹配到的每个文档加独占锁,其他事务对该文档的
updateOne()或findOneAndUpdate()会被阻塞,直到本事务提交或回滚
更轻量的选择:带条件的 updateOne() 实现乐观锁
如果只是做「检查后变更」(如库存扣减、状态流转),不需要事务内复杂计算,直接用 updateOne() 加匹配条件最高效:
const result = await collection.updateOne(
{ _id: ObjectId("..."), stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ writeConcern: { w: "majority" } }
);
if (result.matchedCount === 0) {
// 库存不足,失败
} else if (result.modifiedCount === 0) {
// 条件满足但未修改(罕见,通常因其他并发已改)
}
优势与限制:
- 无需事务开销,不依赖副本集部署,单节点也能用
- 原子性由存储引擎保证:条件匹配 + 修改一步完成
- 不提供隔离性——其他会话可能读到旧
stock值,直到 majority 写入完成 - 失败时需自行重试或降级处理,驱动不自动重试(除非你封装了逻辑)
生产环境容易忽略的硬性前提
所有事务方案都依赖几个常被跳过的基础设施条件:
- 部署必须是副本集(至少 3 节点),独立 mongod 不支持事务
- 所有节点的
featureCompatibilityVersion必须 ≥ 4.0(推荐 ≥ 6.0) - 集合必须使用 WiredTiger 引擎(默认,但迁移旧 MMAPv1 数据库时需确认)
- 事务内操作不能跨分片(除非用分片集群 + 分布式事务,但复杂度和延迟显著上升)
-
findOneAndUpdate()的upsert: true在事务中行为受限:MongoDB 8.1 起,遇到重复键错误不再重试
真正卡住人的往往不是语法,而是副本集配置没生效、FCV 版本滞后、或者误以为单节点也能跑事务——这些都会让 findOneAndUpdate() 看似成功执行,实则完全不加锁。











