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

MongoDB多用户场景下第二及后续用户无法更新保存数据问题

问题描述

开发后端接口时遇到异常:业务逻辑为创建File记录并同步更新关联User文档的Dataset字段,当系统仅存在1个用户时,数据创建、更新流程可正常执行;但当系统存在2个及以上用户时,数据无法正常创建或更新。相关接口实现代码如下:

const createFile = async (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return next(
      new HttpError('Invalid inputs passed, please check your data.', 422)
    );
  }

  const { userId, Dataset } = req.body;

  const createdFile = new File({
    userId,
    Dataset,
  });
  let user;
  try {
    user = await User.findById(userId);
  } catch (err) {
    const error = new HttpError(
      'Creating place failed, please try again 1',
      500
    );
    return next(error);
  }

  if (!user) {
    const error = new HttpError('Could not find user for provided id', 404);
    return next(error);
  }

  try {
    const sess = await mongoose.startSession();
    sess.startTransaction();
    await createdFile.save({ session: sess });
    user.Dataset.push(createdFile);
    await user.save({ session: sess });
    await sess.commitTransaction();
  } catch (err) {
    const error = new HttpError(
      'Creating place failed, please try again.2',
      500
    );
    return next(error);
  }

  res.status(201).json({ files: createdFile });
};

接口运行时抛出的错误信息如下:

Error: User validation failed: _id: Error, expected `_id` to be unique. Value: `62c661c629d1cb99768efd05`
    at ValidationError.inspect (C:\Users\acit\Desktop\FYP Code\FYP Code\backend2\node_modules\mongoose\lib\error\validation.js:48:26)
    at internal/per_context/primordials.js:23:32
    at formatValue (internal/util/inspect.js:783:19)
    at inspect (internal/util/inspect.js:337:10)
    at formatWithOptionsInternal (internal/util/inspect.js:2016:40)
    at formatWithOptions (internal/util/inspect.js:1898:10)
    at console.value (internal/console/constructor.js:323:14)
    at console.log (internal/console/constructor.js:358:61)
    at createFile (C:\Users\acit\Desktop\FYP Code\FYP Code\backend2\controllers\files-controller.js:102:13)
    at processTicksAndRejections (internal/process/task_queues.js:93:5) {
  errors: {
    _id: ValidatorError: Error, expected `_id` to be unique. Value: `62c661c629d1cb99768efd05`
        at validate (C:\Users\acit\Desktop\FYP Code\FYP Code\backend2\node_modules\mongoose\lib\schematype.js:1321:13)
        at C:\Users\acit\Desktop\FYP Code\FYP Code\backend2\node_modules\mongoose\lib\schematype.js:1297:24
        at processTicksAndRejections (internal/process/task_queues.js:93:5) {
      properties: [Object],
      kind: 'unique',
      path: '_id',
      value: new ObjectId("62c661c629d1cb99768efd05"),
      reason: undefined,
      [Symbol(mongoose:validatorError)]: true
    }
  },
  _message: 'User validation failed'
}
故障原因

报错核心是User集合触发了_id唯一约束冲突,说明执行user.save()时,Mongoose没有识别到这是对已存在User文档的更新操作,反而将其判定为新文档的插入操作,尝试写入的_id已经在集合中存在,最终触发报错。
具体触发逻辑:

  • 代码在事务外部查询得到User文档,这个文档没有和后续创建的事务session绑定,属于事务上下文外的游离文档
  • 当在事务中传入session执行user.save()时,Mongoose无法匹配到事务上下文内的文档状态,在多用户场景下缓存状态不一致,就会误将更新操作判定为新文档插入
  • 单用户场景下Mongoose文档缓存刚好能匹配到原有文档状态,因此不会触发该问题,存在多个用户时缓存冲突概率大幅升高,故障稳定复现
  • 额外逻辑缺陷:如果Dataset字段设计为File集合的关联引用字段,直接push完整File文档对象也会导致数据写入不符合预期,额外增加校验冲突概率
修复方案
  • 将User文档的查询操作移到事务内部,查询时绑定当前事务的session,保证文档和事务上下文一致,让Mongoose正确识别这是更新操作而非新插入
  • 明确Dataset字段的存储逻辑:如果是关联File集合的引用数组,只需要push File文档的_id即可,不需要传入完整文档对象;如果是嵌套子文档数组,再传入完整文档结构
  • 补全事务的异常回滚、session关闭逻辑,避免事务资源泄漏

修复后的核心代码段:

const createFile = async (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return next(
      new HttpError('Invalid inputs passed, please check your data.', 422)
    );
  }

  const { userId, Dataset } = req.body;
  let sess;
  try {
    sess = await mongoose.startSession();
    sess.startTransaction();

    // 事务内查询用户,绑定session
    const user = await User.findById(userId).session(sess);
    if (!user) {
      throw new HttpError('Could not find user for provided id', 404);
    }

    const createdFile = new File({ userId, Dataset });
    await createdFile.save({ session: sess });

    // 引用场景下只存储文件ID,嵌套子文档场景可替换为createdFile
    user.Dataset.push(createdFile._id);
    await user.save({ session: sess });

    await sess.commitTransaction();
    res.status(201).json({ files: createdFile });
  } catch (err) {
    if (sess) {
      await sess.abortTransaction();
    }
    const error = new HttpError(
      err.statusCode ? err.message : 'Creating file failed, please try again.',
      err.statusCode || 500
    );
    return next(error);
  } finally {
    if (sess) {
      await sess.endSession();
    }
  }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 16:57:16