事务内读不到刚写数据,需将readConcern设为"snapshot"并作为withTransaction()第二参数传入,单节点不支持该级别,必须启用副本集;WriteConflict(错误码112)是MVCC快照校验失败所致,须精准匹配异常消息重试,同时拆分事务、避免热点。

事务里读不到刚写的数据,先检查 readConcern 是否设为 "snapshot"
事务中 insert 之后 findOne 返回 null,不是代码异步问题,而是默认 readConcern 是 "local",它不保证事务内“写后立即可读”。只有 "snapshot" 级别才能提供事务内一致性快照和 Read-Your-Writes 语义。
- 错误做法:把
{ readConcern: { level: "snapshot" } }塞进findOne()或insertOne()的 options 参数里 —— MongoDB 完全忽略 - 正确位置:必须作为
withTransaction()的第二个参数传入,例如:session.withTransaction(async () => { ... }, { readConcern: { level: "snapshot" } }) - Mongoose 用户注意:
readConcern选项在 v6.0+ 才真正生效;旧版本即使写了也静默失效 - 所有事务内读操作(
find、countDocuments、aggregate)都自动继承该设置,无需每个调用重复写
单节点 MongoDB 启动就报 Snapshot reads require a replica set
这个错误不是配置漏了,是硬性限制:"snapshot" 依赖副本集的 oplog 机制,standalone 模式根本不支持。本地开发时直接 mongod --port 27017 启动,然后在代码里传 readConcern: "snapshot",事务根本不会执行,直接 throw Error。
- 验证方式:连接后运行
rs.status(),返回空对象或报no replset config就是 standalone - 临时开发解法:改用
readConcern: "majority",但需确保副本集已启用enableMajorityReadConcern: true - 生产必须用副本集(哪怕单成员):启动时加
--replSet rs0,连接后手动执行rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "localhost:27017" }] }) - Docker 部署要额外注意:
docker run命令必须带--replSet rs0,且首次启动后必须进 shell 手动rs.initiate()
事务提交失败报 UnknownTransactionCommitResult,重试不能只 retry commit
这个错误表示服务端无法确认事务是否已提交,状态未知。此时若只重试 commitTransaction(),会因 txnNumber 失效而触发 NoSuchTransaction —— 必须重试整个事务体,而非仅提交动作。
- 每次重试前必须调用
session.endSession(),防止 session 泄漏 - 新建
ClientSession,重新startTransaction(),并再次传入{ readConcern: { level: "snapshot" } } - 所有写操作必须幂等:用
updateOne(..., { upsert: true })替代insertOne(),用$setOnInsert+$set控制字段更新时机 - 底层配置必须匹配:所有分片/副本集成员开启
enableMajorityReadConcern=true,MongoDB 小版本号(如 6.0.12 vs 6.0.15)必须完全一致,writeConcern至少为{ w: "majority", j: true }
WriteConflict 错误不是业务错,是 MVCC 快照校验失败
WriteConflict(错误码 112)不是锁等待或权限问题,而是事务提交前发现所依赖的文档快照已被其他事务修改,MVCC 机制直接中止当前事务。它不重试、不阻塞,立刻抛出含 "WriteConflict" 字符串的异常。
- Spring Retry 必须精准匹配消息字符串:
exceptionExpression ="#{message.contains('WriteConflict')}",不能只捕获UncategorizedMongoDbException - 重试策略建议:
maxAttempts = 8,delay = 100,multiplier = 2.0(即 100 → 200 → 400 → … ms),避免雪崩 - 根本缓解方式:拆分大事务、避免单文档热点(比如高频更新同一订单)、将非必要读操作移出事务外
- 注意隐式读:
bulkWrite(..., { upsert: true })会隐式触发查询,同样参与快照校验,也可能引发冲突
readConcern 必须传给 withTransaction() 而不是某个具体操作这一条。

















