MongoDB 实现接口幂等性

MongoDB 实现接口幂等性

一、核心概念:什么是幂等性

定义:同一个请求,执行 1 次 和 执行 N 次,最终业务结果完全一致,不会产生重复数据、重复扣费、重复发货等问题。

业务场景:支付回调、退款回调、订单重试、消息重复投递(高频并发、0.1ms 瞬时双请求)

核心痛点:普通代码 查询 \+ 判断 \+ 修改 是分步操作,非原子,高并发下会穿透,导致重复处理。

二、关键核心:为什么代码判断防不住并发?

错误写法(非幂等、并发必崩)

// 先查
data = find(orderNo)
// 代码判断
if(data未处理){
    // 0.1ms并发瞬间,两个请求都会走到这里
    执行业务()
    更新状态()
}

问题:查询和更新中间有极短时间窗口,并发请求会同时判定为“未处理”,重复执行业务

解决方案:放弃代码分步判断,使用 MongoDB 原生原子操作(数据库层面一步完成,自带行锁)

三、MongoDB 原子操作核心特性(重点)

1. MongoDB 所有单文档操作,都是原子性的

2. 原子操作自带行级排他锁:只锁当前操作的单条文档,不锁表、性能高

3. 锁生命周期极短:操作执行瞬间加锁,执行完毕立即释放

4. 彻底解决 0.1ms 并发重复回调 问题,无需 Redis 分布式锁

四、MongoDB 可实现幂等的所有原子方法

以下方法全部原子、带行锁、支持幂等防重

1. updateOne(最常用、推荐做状态更新)

逻辑:根据「订单号+未处理状态」原子更新,只有首次请求能更新成功

updateOne(
  { orderNo: "xxx", processed: false },
  { $set: { processed: true, handleTime: new Date() } }
)

判重依据:modifiedCount == 1 才执行业务

2. findOneAndUpdate(可查可改,适配复杂场景)

兼具查询和更新能力,原子抢占处理权,适合需要获取旧数据的业务

3. insertOne + 唯一索引(支付场景最优、最简单)

生产最高频方案,零并发漏洞

前置条件:给 orderNo 建立 唯一索引

db.pay_log.createIndex({ orderNo: 1 }, { unique: true })

逻辑:

  • 首次请求:插入成功 → 执行业务

  • 重复/并发请求:触发 E11000 唯一索引冲突报错 → 直接忽略

4. 其他原子方法(了解即可)

  • findOneAndReplace:原子替换整条文档

  • findOneAndDelete:原子删除抢占,适合一次性任务

  • updateMany:批量更新(支付幂等场景极少用)

五、原子操作 & 幂等性的核心关联(面试必问)

为什么原子操作就能实现幂等?

  1. 合并步骤:将「查询判断 + 数据修改」两个易并发穿透的步骤,合并为数据库不可拆分的单步原子操作

  2. 锁竞争拦截:并发请求争抢同一文档行锁,只有一个请求能执行成功

  3. 状态不可逆:首次处理后数据状态变更(已处理/已插入),后续所有重复请求均匹配不到条件,无法二次执行业务

  4. 最终效果:无论请求重复多少次、并发多高,业务只生效一次,完全满足幂等性

六、两套可直接上线的幂等方案(无Redis)

方案一:唯一索引 + insertOne(极简、最稳)

try {
  // 原子抢占,并发只有一条能成功
  db.pay_log.insertOne({ orderNo: "xxx", createTime: new Date() })
  // 插入成功,执行支付核心业务
  doBiz()
} catch (e) {
  // 唯一索引冲突,重复请求,直接忽略
  return "重复回调,无需处理"
}

方案二:updateOne 状态原子更新(可控性强)

let res = db.pay_log.updateOne(
  { orderNo: "xxx", processed: false },
  { $set: { processed: true } }
)
// 只有更新成功才执行业务
if (res.modifiedCount === 1) {
  doBiz()
}

本文由萧兮的博客原创发布,欢迎转载,转载务必保留原文链接。

萧兮的博客https://www.20010515.xyz · 原文:https://www.20010515.xyz/posts/019e4ece-8d56-7f90-b8a5-7599766ede97