javascript 重写Mongoose保存方法以在“重复密钥错误”时重试

roejwanj  于 2023-01-29  发布在  Java
关注(0)|答案(1)|浏览(85)

我的Mongoose模式使用custom_id值,我继承的代码执行类似于以下操作

const sampleSchema = new mongoose.Schema({
  _id: String,
  key: String,
});

sampleSchema.statics.generateId = async function() {
  let id;
  do {
    id = randomStringGenerator.generate({length: 8, charset: 'hex', capitalization: 'uppercase'});
  } while (await this.exists({_id: id}));
  return id;
};

let SampleModel = mongoose.model('Sample', sampleSchema);

一个简单的用法如下:

let mySample = new SampleModel({_id: await SampleModel.generateId(), key: 'a' });
await mySample.save();

这至少有三个问题:

  • 每次保存都需要至少两次访问数据库,一次用于测试唯一ID,另一次用于保存文档。
  • 要使其工作,必须在每次保存之前手动调用generateId(),一个理想的解决方案可以为我处理这个问题,就像Mongoose处理ObjectId类型的id一样。
  • 最重要的是,存在一个可能导致duplicate key error的争用条件。假设有两个客户端运行此代码。两个客户端都碰巧同时生成相同的ID,两个客户端都在数据库中查找并发现ID不存在,两个客户端都尝试将记录写入数据库。第二个客户端将失败。

一个理想的解决方案是,在保存时,生成一个id,将其保存到数据库中,并在重复键错误时,生成一个新的id并重试。循环执行此操作,直到文档成功存储。问题是,我不知道如何让Mongoose让我这样做。
以下是我的尝试:基于这个SO Question,我发现了一个相当老的覆盖save函数以完成类似操作的示例(使用非常老的mongoose版本),并基于它进行了此尝试。

// First, change generateId() to force a collision
let ids = ['a', 'a', 'a', 'b'];
let index = 0;
let generateId = function() {
  return ids[index++];
};

// Configure middleware to generate the id before a save
sampleSchema.pre('validate', function(next) {
  if (this.isNew)
    this._id = generateId();
  next();
});

// Now override the save function
SampleModel.prototype.save_original = SampleModel.prototype.save;
SampleModel.prototype.save = function(options, callback) {
  let self = this;
  let retryOnDuplicate = function(err, savedDoc) {
    if (err) {
      if (err.code === 11000 && err.name === 'MongoError') {
        self.save(options, retryOnDuplicate);
        return;
      }
    }
    if (callback) {
      callback(err, savedDoc);
    }
  };
  return self.save_original(options, retryOnDuplicate);
}

这让我接近但我泄漏了一个承诺,我不知道在哪里。

let sampleA = new SampleModel({key: 'a'});
let sampleADoc = await sampleA.save();
console.log('sampleADoc', sampleADoc); // prints undefined, but should print the document
let sampleB = new SampleModel({key: 'b'});
let sampleBDoc = await sampleB.save();
console.log('sampleBDoc', sampleBDoc); // prints undefined, but should print the document
let all = await SampleModel.find();
console.log('all', all); // prints `[]`, but should be an array of two documents

产出

sampleADoc undefined
sampleBDoc undefined
all []

文档最终会写入数据库,但在调用console.log之前不会写入。
我在哪里泄露了一个承诺?有没有更简单的方法来解决我概述的三个问题?
编辑1: Mongoose 版本:5.11.15

izj3ouym

izj3ouym1#

我通过修改保存覆盖来修复这个问题。完整的解决方案如下所示:

const sampleSchema = new mongoose.Schema({
  _id: String,
  color: String,
});

let generateId = function() {
  return randomStringGenerator.generate({length: 8, charset: 'hex', capitalization: 'uppercase'});
};

sampleSchema.pre('validate', function() {
  if (this.isNew)
    this._id = generateId();
});

let SampleModel = mongoose.model('Sample', sampleSchema);

SampleModel.prototype.save_original = SampleModel.prototype.save;
SampleModel.prototype.save = function(options, callback) {
  let self = this;

  let isDupKeyError = (error, field) => {
    // Determine whether the error is a duplicate key error on the given field
    return error?.code === 11000 && error?.name === 'MongoError' && error?.keyValue[field];
  }

  let saveWithRetries = (options, callback) => {
    // save() returns undefined if used with callback or a Promise otherwise.
    // https://mongoosejs.com/docs/api/document.html#document_Document-save
    let promise = self.save_original(options, callback);
    if (promise) {
      return promise.catch((error) => {
        if (isDupKeyError(error, '_id')) {
          return saveWithRetries(options, callback);
        }
        throw error;
      });
    }
  };

  let retryCallback;
  if (callback) {
    retryCallback = (error, saved, rows) => {
      if (isDupKeyError(error, '_id')) {
        saveWithRetries(options, retryCallback);
      } else {
        callback(error, saved, rows);
      }
    }
  }

  return saveWithRetries(options, retryCallback);
}

这将重复生成_id,直到调用成功的保存并解决原始问题中列出的三个问题:

  • 到数据库的最小行程已经从两个减少到一个。当然,如果有冲突,更多的行程将发生,但这是例外情况。
  • 这个实现负责生成id本身,在保存之前不需要手动执行任何步骤,这降低了复杂性,并消除了原始方法中存在的保存所需的先决条件知识。
  • 竞争条件已解决。如果两个客户端尝试使用相同的密钥也没关系。一个会成功,另一个会生成新密钥并再次保存。

为了改善这一点:

  • 对于一个文档,应该有一个最大的保存尝试次数,在这种情况下,你可能已经用完了你所使用的域中所有可用的键。
  • 唯一字段不能命名为_id,或者您可能有多个字段需要唯一的生成值。可以更新嵌入的帮助函数isDupKeyError()以查找多个键。然后,在出错时,您可以添加逻辑以仅重新生成失败的键。

相关问题