mongodb无select...for update,但可通过事务+findoneandupdate()在wiredtiger引擎下实现文档级逻辑行锁;避免find()+updateone()竞态,推荐条件updateone()乐观锁或事务内原子读改写。

MongoDB 没有传统 SQL 中那种显式的 SELECT ... FOR UPDATE 语句,也无法在事务外对单个文档加“排他读写锁”。但通过事务 + 正确的读写组合,可以在逻辑上达成等效于行级锁定的效果——前提是操作落在同一个文档上,且使用支持文档级并发控制的存储引擎(如 WiredTiger)。
为什么不能直接用 find() + updateOne() 模拟行锁
常见误区是先 find() 读出文档,再根据结果做 updateOne()。这中间存在竞态窗口:两个会话可能同时读到旧值,然后都执行更新,导致后写覆盖前写(lost update)。这不是锁的问题,而是缺少原子性保障。
真正需要的是「读-改-写」三步合一的原子操作,或至少让读操作参与事务快照和锁协商。
- WiredTiger 在事务中对修改的文档自动加文档级
X(独占)锁,其他事务无法同时修改该文档 - 但
find()默认不触发写锁;只有实际执行updateOne()、deleteOne()等写操作时,才会按需加锁 - 若只读不写,事务内
find()不会阻塞其他会话,也不被其他会话阻塞 → 无法形成“锁定等待”效果
用 updateOne() 的 writeConcern + 条件匹配实现乐观锁语义
如果你只是想防止并发修改冲突(比如库存扣减),最轻量的方式是跳过显式事务,直接用带条件的 updateOne():
db.inventory.updateOne(
{ _id: ObjectId("..."), stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ writeConcern: { w: "majority" } }
)
这个操作原子性地检查并更新,返回 result.matchedCount === 1 && result.modifiedCount === 1 才代表成功扣减。失败则重试或报错。
- 无需开启事务,开销低
- 适用于「状态检查+变更」类场景(如订单状态流转、余额扣减)
- 注意:不保证隔离性,其他会话仍可读到旧值,直到 majority 写入完成
在事务中用 findOneAndUpdate() 强制获取文档快照并加锁
当业务逻辑必须基于当前文档状态做复杂判断(比如要读出 price 和 discount 后计算 finalAmount),又要求后续修改不被并发干扰,就得进事务,并用 findOneAndUpdate() 替代分开的 find() + updateOne():
await session.withTransaction(async () => {
const doc = await collection.findOneAndUpdate(
{ _id: id },
{ $set: { updatedAt: new Date() } },
{
returnDocument: "after",
session
}
);
<p>// 基于 doc.value 做业务判断
if (doc.value.status !== "pending") throw new Error("invalid state");</p><p>await collection.updateOne(
{ _id: id },
{ $set: { status: "processed", processedAt: new Date() } },
{ session }
);
});</p>
-
findOneAndUpdate()在事务中会立即对目标文档加X锁,后续同事务内的操作可安全依赖其返回值 - 其他会话对该文档的写操作会被阻塞,直到本事务提交或中止
- 注意:必须传
session,否则不参与事务上下文,也不会获得锁保护 - 如果只读不改,用
findOne()+session不会加锁 → 别指望它能挡住别人写
事务超时与锁持有时间必须严格控制
MongoDB 事务默认 60 秒超时,且锁只在事务活跃期间持有。一旦事务卡住(比如网络延迟、前端没发完请求、代码死循环),不仅操作失败,还会阻塞其他会话对相关文档的写入。
- 所有事务内操作应尽量精简,避免 I/O、HTTP 调用、复杂计算
- 不要在事务里调用外部 API;如有必要,先在外围拿到结果,再进事务执行 DB 变更
- 监控
db.currentOp({ "secs_running": { "$gt": 30 } })可发现长事务 - 分片集群中跨分片事务还会引入两阶段提交开销,锁粒度实际升至分片级别
真正容易被忽略的是:因果一致性会话(causal consistency)不提供行锁,事务不启用就等于没锁,而锁只在 WiredTiger 文档级生效——如果你用的是内存引擎或 mmapv1(已废弃),那根本不存在文档级锁。











