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:批量更新(支付幂等场景极少用)
五、原子操作 & 幂等性的核心关联(面试必问)
为什么原子操作就能实现幂等?
-
合并步骤:将「查询判断 + 数据修改」两个易并发穿透的步骤,合并为数据库不可拆分的单步原子操作
-
锁竞争拦截:并发请求争抢同一文档行锁,只有一个请求能执行成功
-
状态不可逆:首次处理后数据状态变更(已处理/已插入),后续所有重复请求均匹配不到条件,无法二次执行业务
-
最终效果:无论请求重复多少次、并发多高,业务只生效一次,完全满足幂等性
六、两套可直接上线的幂等方案(无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