NodeJS(NestJS)并行请求致MongoDB数据重复问题求解
问题原因
- 先查后插的竞态条件:
invoiceNumber存在性检查与文档创建是两个独立操作,MongoDB不会将其视为原子事务。并行请求会同时通过检查,随后各自执行创建操作,最终生成重复文档。 - 内存锁的局限性:内存锁仅作用于单个进程内,若项目采用多实例部署(如K8s多副本、集群模式),不同实例的内存空间相互隔离,跨实例的并行请求完全不受内存锁约束。
- 锁机制的实现缺陷:如果内存锁的key未精准绑定
invoiceNumber,或者异常场景下锁未正确释放(如未用try/finally包裹),会导致锁失效或死锁,无法彻底阻止重复请求。
可行解决方案
1. 利用MongoDB唯一索引(最彻底的底层保障)
直接在payment集合的invoiceNumber字段上创建唯一索引,从数据库层面阻止重复数据插入:
// 在Payment实体类中用Mongoose装饰器配置唯一索引 import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; @Schema({ indexes: [{ fields: { invoiceNumber: 1 }, unique: true }] }) export class Payment extends Document { @Prop({ required: true }) invoiceNumber: string; // 其他业务字段... } export const PaymentSchema = SchemaFactory.createForClass(Payment);
当重复请求尝试插入相同invoiceNumber的文档时,MongoDB会抛出DuplicateKeyError,在服务层捕获该错误并返回“支付记录已存在”的提示即可。
2. 使用MongoDB原子操作(避免竞态条件)
用findOneAndUpdate的upsert选项,将“检查+创建/更新”合并为单个原子操作:
// 服务层处理逻辑示例 async handlePaymentUpdate(paymentData: PaymentDto) { // 原子执行:存在则更新,不存在则创建 const result = await this.paymentModel.findOneAndUpdate( { invoiceNumber: paymentData.invoiceNumber }, { $set: paymentData }, // 按需指定更新字段 { upsert: true, new: true } // upsert=true触发插入逻辑,new=true返回最终文档 ); return result; }
该操作由MongoDB保证原子性,完全规避竞态条件。
3. 分布式锁替代内存锁
如果必须在业务层做前置拦截,改用分布式锁(如基于Redis的锁),确保跨实例的请求都受锁约束:
// NestJS中Redis分布式锁示例(需先安装@nestjs/redis等依赖) async handlePaymentUpdate(paymentData: PaymentDto) { const lockKey = `payment:lock:${paymentData.invoiceNumber}`; const lockTTL = 5000; // 设置锁过期时间,避免服务崩溃导致死锁 // 尝试获取锁(NX表示仅当key不存在时设置) const lockAcquired = await this.redisClient.set(lockKey, 'locked', 'EX', lockTTL, 'NX'); if (!lockAcquired) { throw new BadRequestException('当前请求正在处理,请稍后重试'); } try { // 执行业务逻辑:检查+创建/更新 let payment = await this.paymentModel.findOne({ invoiceNumber: paymentData.invoiceNumber }); if (!payment) { payment = await this.paymentModel.create(paymentData); } else { payment = await this.paymentModel.findByIdAndUpdate(payment._id, paymentData, { new: true }); } return payment; } finally { // 无论成功失败都释放锁 await this.redisClient.del(lockKey); } }
4. 幂等性处理
要求上游调用方在请求中携带唯一的requestId,服务层先检查该requestId是否已处理过,再执行业务逻辑:
- 在
payment集合中新增requestId字段并创建唯一索引 - 处理请求时,先根据
requestId判断是否已处理,避免重复执行
内容的提问来源于stack exchange,提问作者Harish Jangra
相关产品推荐
相关产品推荐

