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

重写Mongoose save方法以在重复键错误时重试

Mongoose自定义_id问题解决方案

一、Promise泄漏的原因

重写save方法时没有正确返回完整的Promise链是核心问题:

  • 若在捕获重复键错误后发起新的save操作,但未将其作为Promise返回,外层await会提前拿到undefined,而数据库写入仍在后台异步执行,就会出现"console.log提前执行但最终写入成功"的现象。
  • 典型错误示例:
    MySchema.methods.save = function() {
      if (!this._id) {
        this._id = generateId();
      }
      return this.base.Model.prototype.save.call(this)
        .catch(err => {
          if (err.code === 11000) {
            this._id = generateId();
            this.base.Model.prototype.save.call(this); // 未返回该Promise,导致链断裂
          } else {
            throw err;
          }
        });
    };
    

二、更优解决方案(一次性解决三个原始问题)

1. 用pre('save')中间件自动生成ID

无需手动调用generateId(),在保存前自动触发ID生成,解决"手动调用"的问题:

MySchema.pre('save', function(next) {
  if (!this._id) {
    this._id = generateUniqueId(); // 自定义的唯一ID生成函数
  }
  next();
});

2. 重写save方法实现带重试的原子保存(修复Promise泄漏+解决竞态条件)

通过异步循环实现"生成ID-保存-重复则重试"逻辑,确保Promise链完整,同时减少不必要的数据库请求:

MySchema.methods.save = async function(options) {
  const maxRetries = 3; // 设置最大重试次数,避免无限循环
  let retryCount = 0;

  while (retryCount < maxRetries) {
    try {
      if (!this._id) {
        this._id = generateUniqueId();
      }
      // 调用Mongoose原生save方法并返回结果
      return await this.base.Model.prototype.save.call(this, options);
    } catch (err) {
      if (err.code === 11000 && retryCount < maxRetries - 1) {
        // 捕获重复键错误,生成新ID后重试
        this._id = generateUniqueId();
        retryCount++;
      } else {
        // 非重复键错误或达到重试上限,抛出错误
        throw err;
      }
    }
  }
};

该逻辑每次尝试仅发起一次数据库请求(重试场景除外),同时自动处理竞态条件导致的重复键错误,无需手动干预。

3. 额外优化:降低ID重复概率

优化generateUniqueId()逻辑,比如结合时间戳+随机字符串+进程ID,进一步减少重复概率,降低重试次数:

function generateUniqueId() {
  const timestamp = Date.now().toString(36);
  const randomStr = Math.random().toString(36).substr(2, 8);
  const pid = process.pid.toString(36);
  return `${timestamp}-${randomStr}-${pid}`;
}

4. 确保唯一索引配置正确

显式声明_id的唯一索引(Mongoose默认已开启,但显式声明可避免意外):

const MySchema = new mongoose.Schema({
  _id: { type: String, required: true, unique: true },
  // 其他字段...
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 18:15:37