MongoDB不支持SELECT...FOR UPDATE等显式悲观锁语法,因其无SQL式锁命令且不提供行级锁;所谓“事务中模拟悲观锁”实为利用多文档事务、原子写操作及条件匹配(如updateOne带状态校验)实现隐式短暂排他控制,而非传统锁机制。

MongoDB 本身没有 SELECT ... FOR UPDATE 这类显式悲观锁语法,也不支持行级锁或手动加读写锁。所谓“在事务中实现悲观锁效果”,本质是利用 MongoDB 4.0+ 的多文档事务 + 原子性写操作 + 查询条件约束,模拟出“先锁再改”的行为——但这个“锁”是隐式的、短暂的、由事务隔离级别和写操作触发的排他控制,不是传统意义上的锁。
为什么不能直接用 FOR UPDATE 或 lock()
MongoDB 不提供 SQL 风格的显式锁命令;db.collection.find().lock() 不存在;FOR UPDATE 是 MySQL/PostgreSQL 语法,在 MongoDB Shell 或驱动中会报错 ReferenceError: FOR is not defined 或解析失败。所有试图“先查后锁”的中间态(比如查完再发一个锁命令)在分布式环境下天然不安全——查与锁之间存在竞态窗口。
用 updateOne + 匹配条件替代“查+锁+改”
真正可行的做法是跳过“查”,直接用带业务校验的 updateOne 尝试原子修改。这相当于把“锁”逻辑内嵌进写操作里:
- 把库存扣减、状态变更、余额更新等操作,全部写成单条
updateOne({ _id: ..., status: "pending" }, { $set: { status: "processing" } })形式 - 匹配条件必须包含当前预期状态(如
stock: { $gt: 0 }、version: 123、locked: false),否则不生效 - 检查
result.matchedCount和result.modifiedCount:为 0 表示“别人已抢先操作”,需重试或拒绝 - 不要在事务外先
findOne再updateOne——两次操作之间无原子性保障
事务内仍需注意的三个关键点
即使开了 session.startTransaction(),也不等于自动获得悲观锁语义:
-
readConcern: "snapshot"必须显式设置,否则事务内读可能看到其他未提交写(尤其跨分片时) - 事务生命周期越长,持有写锁时间越久,越容易触发
MaxTimeMSExpired或被自动中止;建议把耗时操作(如 HTTP 调用、文件处理)移出事务 - 唯一能阻塞并发写的,只有写操作本身(如
updateOne对同一_id的竞争);对非主键字段(如email)做唯一索引约束,可防止重复插入,但不构成“锁”
真正需要强一致写冲突控制时,别依赖“模拟悲观锁”,优先考虑业务层排队(如 Redis + Lua 原子计数)、预占资源(如生成待支付订单并设 TTL),或者换用支持原生行锁的数据库。MongoDB 的事务设计初衷是“短平快”,不是用来扛秒杀级写竞争的。











