避免 Mongoose `store.save()` 在高并发下的坑:原理解析与最佳实践

1. 背景

在使用 Mongoose 进行数据库操作时,store.save() 是最常见的文档更新方式之一。然而,在高并发环境 下,store.save() 可能会导致 No matching document found for id 错误,并导致某些更新丢失或失败。

本篇文章将深入剖析 store.save() 在高并发下的问题、原理,以及如何正确地避免这个坑。


2. store.save() 在高并发下的问题

2.1 问题现象

当多个请求并发执行 store.save() 时,可能会报如下错误:

bash 复制代码
No matching document found for id "66d1dcc7c685ab6bb92a3098" version 512381

这个错误意味着 Mongoose 找不到匹配的文档 ,通常是因为文档的 __v 版本号发生了变化

2.2 代码示例

假设有如下代码:

csharp 复制代码
async getDelayHours(){
    const store = await this.ctx.model.Store.findOne({});
    const delayHours = store.postTaskRunAtDelayHours.shift();
    store.postTaskRunAtDelayHours.push(delayHours);
    await store.save();
    return delayHours;
}

多个并发请求调用 getDelayHours() 时,所有请求都获取了相同的 store,并且版本号 __v 也是一样的

当第一个请求执行 store.save() 时,它会成功,并且 __v 版本号增加 。但其他请求的 __v 版本号已过时,导致 save() 失败,触发 No matching document found for id 错误。


3. 原理解析

3.1 __v 版本字段的背景知识

在 Mongoose 中,每个文档默认都有一个 __v 字段,这是 Mongoose 内部的版本控制字段 。当 save() 发生时,Mongoose 会自动递增 __v,以确保数据的 乐观锁(Optimistic Locking) 机制。

__v 主要用于:

  • 防止并发冲突:当多个请求尝试修改同一文档时,确保只有最新的修改能成功。
  • 确保数据一致性:避免旧数据覆盖新数据,减少数据丢失风险。

如果不想使用 __v,可以在 Schema 配置中关闭:

php 复制代码
const schema = new mongoose.Schema({
  name: String
}, { versionKey: false });

但通常不建议关闭 __v,因为它在并发控制方面起到了重要作用。

3.2 store.save() 的工作方式

store.save() 的底层逻辑如下:

  1. findOne() 读取文档 (此时 __v = X)。

  2. 本地修改文档的某些字段

  3. 调用 save() 时,Mongoose 生成 update 语句 ,并携带 __v = X 作为条件:

    php 复制代码
    db.store.updateOne(
        { _id: ObjectId("66d1dcc7c685ab6bb92a3098"), __v: X },
        { $set: { postTaskRunAtDelayHours: [...], __v: X+1 } }
    );
  4. 如果 __v 仍然是 X,更新成功

  5. 如果 __v 已被其他请求改为 X+1,当前 save() 失败

3.3 为什么 __v 版本冲突会导致 store.save() 失败?

因为 Mongoose 采用乐观锁(Optimistic Locking) ,即:

  • 任何修改操作都会基于 __v 版本号
  • 如果 __v 版本号不匹配,save() 失败,防止并发写入时数据错乱。

但这样会导致高并发环境下 save() 失败率很高


4. 最佳实践:如何避免 store.save() 的并发问题?

4.1 使用 findOneAndUpdate() 进行原子操作(推荐 ✅)

store.save() 相比,findOneAndUpdate() 直接在数据库中执行更新 ,避免 __v 版本冲突。

优化代码

csharp 复制代码
async getDelayHours() {
  const store = await this.ctx.model.Store.findOneAndUpdate(
    {},
    { $pop: { postTaskRunAtDelayHours: -1 } },
    { new: true }
  );

  if (!store) throw new Error("Store not found");

  const delayHours = store.postTaskRunAtDelayHours[store.postTaskRunAtDelayHours.length - 1];
  return delayHours;
}

优点:

  • 避免 store.save() 带来的 __v 版本冲突
  • 原子操作 ,保证多个请求不会取到相同的 delayHours
  • 性能更优 ,减少 findOne() + 修改 + save() 的查询开销。

4.2 事务(Transactions)+ session 机制

如果你的 MongoDB 支持事务(Transactions) ,可以使用 session 保证数据一致性。

ini 复制代码
async getDelayHours() {
  const session = await this.ctx.model.Store.startSession();
  session.startTransaction();

  try {
    const store = await this.ctx.model.Store.findOne({}).session(session);
    const delayHours = store.postTaskRunAtDelayHours.shift();
    store.postTaskRunAtDelayHours.push(delayHours);
    await store.save({ session });
    await session.commitTransaction();
    session.endSession();
    return delayHours;
  } catch (error) {
    await session.abortTransaction();
    session.endSession();
    throw error;
  }
}

优点

  • 事务保证数据一致性,防止并发操作导致数据丢失。
  • 适用于修改多个文档的情况

⚠️ 注意:MongoDB 事务需要 Replica Set,单机模式下可能不支持。


5. 结论

方法 并发安全 性能 适用场景
store.save() ❌ 有 __v 版本冲突 低并发
findOneAndUpdate() ✅ 原子操作 高并发 & 单字段更新
session + save() ✅ 事务保证 需要事务支持

最终建议:

  • 高并发环境下,避免 store.save(),推荐 findOneAndUpdate()
  • 如果有事务需求,可使用 session 机制。

🚀 希望这篇文章能帮助你优化 Mongoose 并发处理,让你的数据库操作更稳定高效!

相关推荐
不叫猫先生几秒前
Amazon Lambda:无服务器时代的计算革命,解锁多样化应用场景
服务器·数据库·人工智能·amazon lambda
小华同学ai1 分钟前
Github 2.3k star 太牛x,京东(JoyAgent‑JDGenie)这个开源项目来得太及时啦,端到端多智能体神器!!!
前端·后端·github
讨厌吃蛋黄酥3 分钟前
前端跨域难题终结者:从JSONP到CORS,一文搞定所有跨域问题!
前端·javascript·后端
cxyxiaokui0017 分钟前
Exception和Error:一场JVM内部的“家庭伦理剧”
后端·面试
义达8 分钟前
Django环境下使用wsgi启动MCP服务
后端·django·mcp
用户4099322502129 分钟前
如何在FastAPI中巧妙实现延迟队列,让任务乖乖等待?
后端·ai编程·trae
码事漫谈13 分钟前
为什么我们应该避免使用 abort、exit、getenv 和 system?
后端
码事漫谈14 分钟前
为什么动态内存分配在关键系统中被视为“不合规”?
后端
_風箏15 分钟前
Ollama【部署 02】Linux本地化部署及SpringBoot2.X集成Ollama(ollama-linux-amd64.tgz最新版本 0.6.2)
人工智能·后端·ollama
张同学的IT技术日记17 分钟前
详细实例说明+典型案例实现 对动态规划法进行全面分析 | C++
后端