MongoDB/Mongoose模型创建时生成唯一属性的最佳实践与重复处理
创建MongoDB/Mongoose模型时生成唯一属性的最佳实践
嘿,这个问题太接地气了——我之前在项目里直接用default生成唯一值的时候,踩过好几次重复键的坑,尤其是并发量上来之后,完全靠生成函数的随机性根本顶不住。下面我分享几个从实践里摸出来的靠谱方案,你可以根据自己的场景选:
1. 用Pre-Save中间件循环检测(低并发场景首选)
如果你的系统并发不高,用Mongoose的pre('save')钩子是最直观的方式:在保存文档前,生成候选值,然后去数据库里查有没有重复,要是有就重新生成,直到找到唯一的为止。
示例代码:
const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); // 用uuid生成足够唯一的字符串,比自己写靠谱 const MySchema = new mongoose.Schema({ name: { type: String }, uniqueProperty: { type: String, unique: true } }); // Pre-save钩子处理唯一值生成 MySchema.pre('save', async function(next) { // 只有当uniqueProperty为空的时候才生成(避免更新时覆盖已有值) if (!this.uniqueProperty) { let isUnique = false; let candidateValue; while (!isUnique) { candidateValue = uuidv4(); // 这里替换成你的自定义生成函数 // 检查数据库里是否已有这个值 const existingDoc = await this.constructor.findOne({ uniqueProperty: candidateValue }); if (!existingDoc) { isUnique = true; this.uniqueProperty = candidateValue; } } } next(); }); const MyModel = mongoose.model('MyModel', MySchema);
⚠️ 注意:这个方案在高并发下可能会有竞态条件——比如两个请求同时生成同一个值,都通过了检查,然后同时保存,这时候数据库的唯一索引会抛出E11000错误。所以高并发场景得看下面的方案。
2. 唯一索引+重试机制(高并发场景必备)
不管你用什么生成逻辑,数据库的唯一索引都是最后一道防线。我们可以捕获保存时的重复键错误,然后自动重试生成新值,这样就能解决竞态条件的问题。
示例代码:
// 先定义模型(和上面一样,不用pre-save钩子也可以) const MySchema = new mongoose.Schema({ name: { type: String }, uniqueProperty: { type: String, unique: true } }); const MyModel = mongoose.model('MyModel', MySchema); // 封装一个带重试的保存方法 async function saveDocumentWithRetry(doc, maxRetries = 5) { let retries = 0; while (retries < maxRetries) { try { // 生成唯一值(如果还没生成的话) if (!doc.uniqueProperty) { doc.uniqueProperty = uuidv4(); // 替换成你的生成函数 } return await doc.save(); } catch (err) { // 捕获重复键错误 if (err.code === 11000 && err.keyPattern.uniqueProperty) { retries++; // 清空旧的重复值,准备生成新的 doc.uniqueProperty = undefined; if (retries === maxRetries) { throw new Error(`尝试了${maxRetries}次仍无法生成唯一值,请稍后重试`); } } else { // 其他错误直接抛出 throw err; } } } } // 使用方式 const newDoc = new MyModel({ name: 'Test' }); saveDocumentWithRetry(newDoc) .then(savedDoc => console.log('保存成功:', savedDoc)) .catch(err => console.error('保存失败:', err));
这个方案的优势是:哪怕并发量高,只要重试次数足够,基本能保证最终生成唯一值,而且数据库的唯一索引能兜底,不会出现重复数据。
3. 用天然唯一的标识符(如果业务允许)
如果你的uniqueProperty不需要自定义格式,那直接用MongoDB内置的_id(ObjectId)就完全够用,它本身就是全局唯一的。或者如果需要自增的唯一值,可以用计数器集合来实现原子性的自增操作,比如:
// 先定义计数器模型 const CounterSchema = new mongoose.Schema({ modelName: { type: String, unique: true }, currentCount: { type: Number, default: 0 } }); const Counter = mongoose.model('Counter', CounterSchema); // 在目标模型的pre-save钩子里面获取自增ID MySchema.pre('save', async function(next) { if (!this.uniqueProperty) { // 原子性递增计数器 const counter = await Counter.findOneAndUpdate( { modelName: 'MyModel' }, { $inc: { currentCount: 1 } }, { upsert: true, new: true } ); this.uniqueProperty = `MY-${counter.currentCount}`; // 自定义格式 } next(); });
这个方案完全不会有重复的问题,因为计数器的递增是原子操作,适合需要有序唯一值的场景。
最后几个关键注意事项
- 一定要加唯一索引:不管用什么方案,
unique: true这个配置不能少,这是数据库层面的最后一道防线,避免任何情况下的重复数据。 - 生成函数要靠谱:尽量用成熟的库(比如uuid)来生成随机字符串,别自己写简单的随机逻辑,不然很容易重复。
- 控制重试次数:高并发场景下的重试别无限循环,设置一个合理的最大重试次数,避免死循环。
内容的提问来源于stack exchange,提问作者Seltsam
相关产品推荐
相关产品推荐

