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

多实例Node.js应用中MongoDB生成存储唯一序列的实现方案

解决多实例部署下MongoDB唯一reference_id生成问题

核心问题分析

你遇到的重复问题本质是并发场景下的竞态条件:多个实例同时读取当前的sequence_number,各自加1后生成相同的reference_id,而mongoose的unique属性只是插入时的约束,无法在生成阶段避免重复。下面是几个落地可行的解决方案:


方案1:用MongoDB原子操作维护序列(推荐)

通过单独的序列集合+原子递增操作,从数据库层面保证每次获取的sequence_number唯一,彻底解决并发问题。

步骤1:创建序列计数器集合

定义一个sequence_counters集合,用来存储每个分类组合的当前序列值:

const mongoose = require('mongoose');

const SequenceCounterSchema = new mongoose.Schema({
  _id: { type: String, required: true }, // 用`order_category_id-order_sub_category_id`作为唯一键
  sequence_value: { type: Number, default: 0 }
});

const SequenceCounter = mongoose.model('SequenceCounter', SequenceCounterSchema);

步骤2:原子获取下一个序列值

使用findOneAndUpdate结合$inc操作符,这个操作是MongoDB层面原子执行的,同一时间只有一个请求能成功递增序列:

async function getNextSequence(categoryId, subCategoryId) {
  const counterKey = `${categoryId}-${subCategoryId}`;
  // upsert: true 表示如果不存在该分类的计数器则自动创建
  // new: true 表示返回更新后的文档
  const counter = await SequenceCounter.findOneAndUpdate(
    { _id: counterKey },
    { $inc: { sequence_value: 1 } },
    { new: true, upsert: true }
  );
  return counter.sequence_value;
}

步骤3:生成唯一reference_id

在写入detail集合前,调用上述方法获取序列值:

// 示例写入逻辑
async function createDetailDocument(data) {
  const { order_category_id, order_sub_category_id } = data;
  const sequenceNumber = await getNextSequence(order_category_id, order_sub_category_id);
  const reference_id = `${order_category_id}${order_sub_category_id}${sequenceNumber}`;

  const detailDoc = new Detail({
    ...data,
    reference_id,
    sequence_number: sequenceNumber
  });

  await detailDoc.save();
  return detailDoc;
}

方案2:添加唯一复合索引+重试机制

如果不想新增序列集合,可以给detail集合的reference_id添加唯一索引,同时在插入失败时重试生成。

步骤1:创建唯一索引

确保reference_id的唯一索引生效(之前可能索引未正确创建):

const DetailSchema = new mongoose.Schema({
  order_category_id: String,
  order_sub_category_id: String,
  sequence_number: Number,
  reference_id: { type: String, unique: true }
});

// 显式创建唯一索引,避免mongoose自动创建失败
DetailSchema.index({ reference_id: 1 }, { unique: true });

const Detail = mongoose.model('Detail', DetailSchema);

步骤2:插入时捕获重复键错误并重试

当并发导致重复reference_id时,MongoDB会抛出E11000重复键错误,此时重试生成即可:

async function createDetailDocumentWithRetry(data, retryCount = 3) {
  try {
    const { order_category_id, order_sub_category_id } = data;
    // 先查询当前最大的sequence_number
    const latestDoc = await Detail.findOne(
      { order_category_id, order_sub_category_id },
      { sequence_number: 1 }
    ).sort({ sequence_number: -1 });

    const sequenceNumber = latestDoc ? latestDoc.sequence_number + 1 : 1;
    const reference_id = `${order_category_id}${order_sub_category_id}${sequenceNumber}`;

    const detailDoc = new Detail({ ...data, reference_id, sequence_number });
    return await detailDoc.save();
  } catch (err) {
    if (err.code === 11000 && retryCount > 0) {
      // 重复键错误,重试
      await new Promise(resolve => setTimeout(resolve, 100));
      return createDetailDocumentWithRetry(data, retryCount - 1);
    }
    throw err;
  }
}

注意:这个方案的可靠性略低于方案1,因为查询最大序列值和插入之间仍存在极小的竞态窗口,适合并发量不极高的场景。


方案3:分布式锁(适合已有Redis的场景)

如果你的系统已经部署了Redis,可以用分布式锁来保证同一分类组合下只有一个请求能生成sequence_number:

const redis = require('redis');
const redisClient = redis.createClient();

async function getNextSequenceWithLock(categoryId, subCategoryId) {
  const lockKey = `lock:seq:${categoryId}-${subCategoryId}`;
  const lockTimeout = 5000; // 锁超时时间,防止死锁

  // 尝试获取锁
  const lockAcquired = await redisClient.set(lockKey, 'locked', {
    EX: lockTimeout,
    NX: true
  });

  if (!lockAcquired) {
    // 未获取到锁,等待后重试
    await new Promise(resolve => setTimeout(resolve, 100));
    return getNextSequenceWithLock(categoryId, subCategoryId);
  }

  try {
    // 查询当前最大序列值
    const latestDoc = await Detail.findOne(
      { order_category_id: categoryId, order_sub_category_id: subCategoryId },
      { sequence_number: 1 }
    ).sort({ sequence_number: -1 });

    return latestDoc ? latestDoc.sequence_number + 1 : 1;
  } finally {
    // 释放锁
    await redisClient.del(lockKey);
  }
}

这个方案需要额外维护Redis,复杂度较高,仅当你已有Redis基础设施时考虑。


内容的提问来源于stack exchange,提问作者Batman

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 13:20:50