重写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
相关产品推荐
相关产品推荐

