wiredtiger锁粒度为文档级,即对任一写操作(含$set、$push、$inc等)均锁定整个文档,不支持字段或数组元素级细粒度锁;冲突时触发乐观重试,而非阻塞等待。

WiredTiger 默认是文档级锁,但“文档级”不等于“单个字段级”,也不是无锁 —— 冲突仍会发生,尤其在更新嵌套字段、数组元素或使用 $inc/$push 等原子操作时。
WiredTiger 的锁粒度到底锁什么?
WiredTiger 在 3.2+ 版本中确实用文档级(document-level)锁替代了 MMAPv1 的表级锁,但这不意味着整个文档被“独占锁定”后其他操作就完全绕道。实际行为更精细:
- 对文档整体替换(如
updateOne({ _id: x }, { $set: { ... } }))会获取该文档的写锁,期间其他写操作必须等待; - 对部分字段更新(如
$set: { "profile.name": "Alice" })也锁整文档,因为 WiredTiger 不支持字段级锁; - 对数组元素操作(如
$push: { tags: "new" }或$addToSet)同样触发文档级写锁 —— 即使只改一个元素,整个文档不可被并发写; - 读操作(
find)默认不阻塞写,但若启用了快照读(readConcern: "snapshot")且事务活跃,可能因版本链检查而短暂延迟。
关键点:updateOne 和 findOneAndUpdate 都走同一套锁路径,区别只在返回值,不改变锁行为。
为什么 findOneAndUpdate 不能自动解决并发冲突?
它只是原子性地“查 + 改 + 写”,但不会帮你做版本校验或重试逻辑 —— 这仍是应用层责任。常见误判:
- 以为加了
upsert: true就能防覆盖:错,多个并发 upsert 同一_id仍可能因竞态导致数据丢失(尤其配合$inc时); - 依赖
result.matched_count === 0判断冲突:这只能说明没匹配到旧文档,无法区分是“真不存在”还是“被别人刚删/改过”; - 忽略
writeConcern影响:比如设了{ w: "majority" },即使锁成功,确认延迟也会拉长临界区,间接加剧排队。
真正能降低冲突概率的是把“读-改-写”压缩进一次 findOneAndUpdate 调用,并用条件字段(如 version)做前置过滤,而不是靠它自动兜底。
什么时候必须上乐观锁?
不是所有场景都需要手动加 version 字段,但以下情况不加就容易丢数据:
- 业务逻辑涉及“读取当前值 → 计算新值 → 写回”三步(例如库存扣减、计数器累加),且不允许最终一致;
- 多个微服务或进程共享同一集合,无法统一协调事务边界;
- 使用
updateMany批量更新时,目标文档集存在交集(比如按标签更新,不同请求筛选条件重叠); - 驱动或 ORM 封装了自动重试,但没暴露底层
matched_count或modified_count,导致你误以为“执行了就成功”。
注意:version 字段必须建索引(createIndex({ version: 1 })),否则条件查询会全表扫,锁住更多无关文档。
事务里锁的行为有变化吗?
有,但仅限于多文档事务(multi-document transaction)。单文档操作(哪怕带 session)仍走普通文档锁,事务本身不降级锁粒度:
- 事务内第一个写操作会为涉及文档加写锁,直到
commitTransaction或abortTransaction; - 事务中读操作(
find)默认使用事务快照,不触发新锁,但会阻塞其他事务对同一文档的写; - 事务超时(
maxTimeMS)或写冲突(WriteConflict错误)会导致自动 abort,此时应用必须捕获TransientTransactionError并重试 —— 不是重试语句,而是重试整个事务逻辑。
真实坑点:WiredTiger 的写冲突错误(WriteConflict)不是每次都会抛出,它依赖存储引擎内部版本比对,有时静默重试 3 次后才失败。所以日志里看不到报错 ≠ 没发生冲突。











