应建唯一索引并用 updateone+upsert替代insertone,重试前先查traceid;事务断连后需查业务状态判断是否跳过;扣减须绑定orderid等业务单据;withtransaction中须await所有操作且捕获异常。

事务里写入后又重试,insertOne 直接报错重复键怎么办
MongoDB 事务本身不自动拦截重复执行,insertOne 遇到唯一索引冲突会抛 DuplicateKeyError,事务直接中止。这不是事务设计缺陷,而是它默认信任你已控制好执行逻辑——但实际网络超时、客户端重试、服务重启都可能触发非预期重入。
核心解法不是靠事务兜底,而是提前用业务主键+唯一索引做幂等约束:
- 在集合上建唯一索引,字段必须覆盖你能识别“同一操作”的所有维度,比如
{ orderId: 1, version: 1 }或{ traceId: 1 } - 事务内改用
updateOne+upsert: true替代insertOne,即使重复执行也只写入一次 - 避免用自增 ID 或时间戳当唯一依据——它们无法保证跨重试的一致性
session.startTransaction() 后没提交就断连,下次怎么判断该重试还是跳过
MongoDB 不提供事务状态查询接口,session 断开后,服务端残留的事务最多挂起 60 秒(默认 transactionLifetimeLimitSeconds),之后自动 abort。但客户端无法得知它是否已提交成功。
安全做法是:所有关键写操作必须带可验证的业务状态字段,例如:
- 插入订单时,同时写入
status: "pending"和createdAt - 重试前先查
findOne({ traceId: "xxx" }),如果找到且status不是初始态,说明已生效,直接跳过 - 不要依赖
_id生成时间或顺序判断“是否第一次”,分布式环境下不可靠
用 findAndModify 做原子扣减,为什么并发下库存还是超卖
findAndModify 在单文档层面是原子的,但如果你的事务里混合了多文档操作(比如扣库存 + 写日志 + 更新用户积分),而只对库存文档加了条件检查,其余操作没做幂等防护,整体就不是业务级原子。
典型陷阱:
- 事务里调用
findAndModify({ stock: { $gt: 0 } }, { $inc: { stock: -1 } }),看似安全,但如果事务中途失败回滚,下次重试会再次扣减——因为没记录“这次扣减对应哪次下单” - 正确做法:把扣减动作和业务单据绑定,例如用
updateOne({ orderId: "abc", stock: { $gt: 0 } }, { $set: { status: "locked" }, $inc: { stock: -1 } }),确保同一orderId只能成功一次 - 别省略
writeConcern: { w: "majority" },否则副本集未同步就返回成功,重试时主节点可能还没收到上次写入
Node.js 里用 withTransaction 包裹异步操作,为什么有时根本进不到回调里
withTransaction 是语法糖,底层仍依赖 session 生命周期。常见静默失败场景:
- 事务函数里用了未
await的 Promise(比如漏了await collection.insertOne(...)),导致函数提前返回,事务被当作空操作提交 - 异步操作抛出未被捕获的异常(如网络错误、JSON 序列化失败),
withTransaction会 abort 事务但不 rethrow,外层看不到错误 - 事务内调用了其他未绑定当前 session 的数据库操作(比如另开了一个
db.collection()),那些操作根本不在事务中
务必在事务函数末尾显式 return 一个 Promise,并用 try/catch 包住所有 await 表达式。
traceId、orderId、requestId 这些字段必须在请求入口就生成并透传到底层,任何一层丢失,幂等就断链。










