mongodb事务重试仍写重复数据,因事务本身不保证幂等性,网络超时或客户端崩溃导致重复提交;必须依赖唯一索引+upsert组合实现单次操作级幂等,而非依赖事务重试机制。

事务重试时 MongoDB 为什么还会写重复数据
MongoDB 的多文档事务本身不自动保证幂等性,只保证原子性和回滚能力。当网络超时、客户端崩溃或驱动主动重试时,事务可能被重复提交——而 MongoDB 无法识别“这是上次那个没收到响应的事务”,只要 session 不一致或 transactionNumber 未被服务端持久跟踪,它就当作全新事务执行。尤其在 writeConcern: { w: "majority" } 下,主节点已提交但响应丢失,客户端重发,副本集就会执行两次。
用 upsert + 唯一索引是最直接的落地方式
不要依赖事务重试逻辑来防重,而是把幂等性下沉到单次写操作本身。核心是让“插入或更新”变成“有则更新、无则插入”,且靠数据库强制约束拒绝冲突:
- 在业务关键字段(如
order_id、payment_no、message_id)上建唯一索引:db.orders.createIndex({ order_id: 1 }, { unique: true }) - 所有写入统一走
updateOne+upsert: true,而不是insertOne:db.orders.updateOne( { order_id: "ORD-2026-001" }, { $set: { status: "paid", amount: 99.9 } }, { upsert: true } ) - 避免在事务中用
insertOne后再查再改——这会引入竞态窗口,且事务内查不到自己未提交的插入
事务内做幂等检查必须配合 session 和 retryableWrite
如果业务逻辑强依赖事务上下文(比如扣库存 + 生单 + 记流水三者必须原子),又需要容忍重试,就得靠客户端和驱动协同:
- 启用
retryWrites: true(4.0+ 默认开启),让驱动自动处理网络层重试,但注意:它只对单文档操作生效,不适用于多文档事务 - 手动管理
ClientSession,并确保每次重试都复用同一个session对象(不能新建) - 在事务开始前,先用该
session执行一次轻量findOne检查是否已存在(例如查order_id),存在则直接跳过整个事务体 - 事务体内部仍要用
upsert,双重保险——session 复用防跨请求重试,upsert 防同 session 内异常分支重复执行
别把 Redis 锁当成事务幂等的银弹
用 Redis 做请求去重(比如存 req_id + TTL)在 HTTP 接口层有效,但嵌入 MongoDB 事务时反而容易出问题:
- Redis 操作和 MongoDB 事务不在同一原子域,Redis 成功但事务失败 → 锁残留,后续请求被误拒
- Redis 失联或超时,锁没设上,事务照常执行 → 重复写入
- 分布式环境下,Redis 不是强一致存储,
SET NX EX在集群模式下有边界 case(如 slot 迁移期间) - 真正要保事务级幂等,唯一可靠锚点只有 MongoDB 自身的唯一索引 + upsert 语义,其他都是辅助手段











