You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 07:18:27