findAndModify 在 3.0+ WiredTiger 下仅锁定匹配的单个文档,依赖文档级互斥锁;并发操作不同文档无冲突,同文档才阻塞;排序、索引缺失或分片不影响锁粒度;返回 null 是因条件不匹配而非锁导致;7.0+ 已移除,须迁至 findOneAndUpdate。

findAndModify 在 3.0+ WiredTiger 引擎下到底锁什么
它只锁匹配到的那一个文档,不是整个集合,更不是整个数据库。这是关键前提——findAndModify 的原子性依赖的是文档级锁,不是行锁或页锁那种抽象概念,而是 WiredTiger 实际加在 BSON 文档内存映射页上的互斥锁。
这意味着:多个并发 findAndModify 请求只要操作的是不同文档(比如不同 _id),就完全不冲突;只有当它们命中同一个文档时,后到的请求才会被阻塞,直到前一个释放锁。
常见误判点:
- 以为加了
{ sort: { _id: 1 } }就能“排队”安全执行——错。排序只影响选哪个文档,不改变锁粒度 - 以为索引缺失会导致全表扫描进而锁住更多文档——不会。WiredTiger 的文档锁是精确到命中的那个文档,扫描过程本身不加写锁
- 在分片集群中误以为锁跨分片生效——实际锁只发生在目标分片的主节点上,其他分片完全无关
为什么高并发下大量返回 null 或 not found
这不是锁导致的失败,而是查询条件没匹配上——最典型场景是:多个并发请求用相同 query(比如 { status: "pending" })+ 相同 sort(比如 { _id: 1 }),但第一个请求把唯一匹配的文档改掉了,后续请求查不到满足条件的文档,于是返回 null(new: false 时)或报 not found 错误。
这和锁无关,是业务逻辑设计问题。例如发券场景中,如果所有 goroutine 都查 { status: "unissued" },但没加 limit 或唯一约束,就必然出现“抢空”后集体失败。
实操建议:
- 确保
query能唯一命中一个待处理文档(如带_id或业务唯一键) - 避免纯状态字段查询(如仅靠
status: "pending")作为并发入口点 - 若必须批量取待处理项,改用
findOneAndUpdate+arrayFilters操作队列式数组字段,而非多轮findAndModify
执行时间长会放大阻塞感知,但不延长锁持有时间
findAndModify 的锁只在“查找 → 修改 → 返回”这个原子路径内持有。一旦写入完成、文档落盘(WiredTiger journal 写完),锁立刻释放。耗时长的操作(比如大文档更新、复杂 $set 表达式、触发大量索引更新)只会让单次调用变慢,但不会让锁“卡住”更久——锁时长 ≈ 写入延迟,不是整个函数执行时间。
真正拖慢并发吞吐的,往往是以下情况:
- 更新字段上有多个未优化的索引,每次修改都要同步刷多份索引页
- 文档体积过大(> 16MB 会直接失败;接近 16MB 时内存拷贝和序列化开销陡增)
- 使用了
$push/$addToSet等可能触发文档重写的操作,导致 WiredTiger 必须分配新空间并迁移数据
MongoDB 7.0+ 已移除 findAndModify,必须迁移到 findOneAndUpdate
从 MongoDB 7.0 开始,findAndModify 命令已被彻底移除,驱动层调用会直接报错 CommandNotFound。这不是兼容性警告,是硬性失效。
findOneAndUpdate 行为几乎一致,但有几点关键差异必须调整:
-
sort参数必须是文档,不能是数组(旧版支持["+id"],新版必须写成{ _id: 1 }) -
fields参数已废弃,改用projection - 返回结构不同:
findOneAndUpdate默认返回修改后的文档(即旧版new: true行为),要获取修改前需显式设returnDocument: "before" - 错误码更精细,比如
WriteConflict可能被抛出(尤其在事务中),需捕获重试而非静默忽略
最容易被忽略的是:很多老代码依赖 findAndModify 的 shell helper 行为(比如自动补全 query 为空对象),迁移到 findOneAndUpdate 后若没传 query,会因参数校验失败而报错,而不是静默执行。


















