必须用upsert+唯一索引防重,因事务不提供幂等性,重试时服务端视作新事务,仅靠重试逻辑无法避免重复写入。

事务重试时为什么还会写重复数据
MongoDB 事务本身不识别“这是上次那个没收到响应的请求”,只管执行。网络超时、客户端崩溃或驱动自动重试,都可能导致同一个业务逻辑被多次提交——而服务端只要 session 不同、transactionNumber 没被持久跟踪,就当全新事务处理。尤其在 writeConcern: { w: "majority" } 下,主节点已提交但响应丢失,客户端重发,副本集就会执行两次。
必须用 upsert + 唯一索引,而不是靠事务重试防重
事务只保证原子性和回滚能力,不提供幂等性。把防重责任交给数据库底层约束,才是可靠路径:
- 在业务关键字段(如
order_id、payment_no)上建唯一索引:db.orders.createIndex({ order_id: 1 }, { unique: true }) - 所有写入统一走
updateOne+upsert: true,禁用insertOne - 避免在事务中先
find再insert——这会引入竞态窗口,且事务内查不到自己未提交的插入
事务体内部也要幂等,不能只靠外部重试逻辑
即使你手动管理 ClientSession 并复用它,也不能完全规避重复执行风险。事务体自身必须可多次运行无副作用:
- 用
$setOnInsert控制初始值,而非无条件$set - 扣减库存类操作,必须绑定业务单据 ID(如
{ orderId: "abc", stock: { $gt: 0 } }),确保同一单据只能成功一次 - 读操作必须在新 session 中重新执行,不能缓存旧结果;若需一致性快照,显式设置
readConcern: "snapshot"
别把 Redis 锁当事务幂等的兜底方案
Redis 和 MongoDB 不在一个原子域里,两者配合反而容易出问题:
- Redis 成功但事务失败 → 锁残留,后续合法请求被误拒
- Redis 失联或超时 → 锁没设上,事务照常执行 → 重复写入
- 集群模式下
SET NX EX在 slot 迁移期间可能失效
真正能托底的只有 MongoDB 自身的唯一索引 + upsert 语义。其他都是辅助手段,且要清楚它们的断裂点在哪里。











