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

如何避免连续执行GraphQL删除图片Mutation时的竞态条件?

搞定并发删除时的用户更新失败问题

哥们,你这问题我太熟了——这是Mongoose乐观锁机制导致的并发更新冲突!咱们先掰扯清楚为啥会这样,再给你几个靠谱的解决办法。

为啥会出这问题?

你看哈,当你快速连续触发5次删除请求时:

  1. 所有请求几乎同时从数据库里读出来同一个版本的用户文档(比如日志里的版本116)
  2. 第一个请求修改完用户的photos数组,保存成功后,用户文档的版本号自动升到117
  3. 后面4个请求还拿着旧版本(116)的文档去保存,Mongoose一看:“不对啊,我数据库里的版本已经是117了,你这旧版本的修改我不能认”,直接就抛出那个版本不匹配的错误了。

而且你代码里context.currentUser是请求一开始就拿到的,后面再查UserModel.findById(currentUser.id),因为请求并发得太急,拿到的还是旧版本的用户数据,自然会冲突。

靠谱的解决办法

1. 用MongoDB原子更新(最推荐!)

别再先查用户、改数组、再保存了,直接用MongoDB的$pull原子操作,让数据库自己去处理数组元素的移除——这玩意儿是在服务器端原子执行的,根本不会有并发冲突的问题,还比你原来的写法高效。

把你修改用户的那段代码换成这个:

// 原子性从用户的photos数组中移除指定ID
await UserModel.findByIdAndUpdate(
  currentUser.id,
  { $pull: { photos: id } }
);

就这么简单!不管你多快连续删,每个请求都会正确地把对应ID从用户数组里拿掉。

2. 加重试机制(适合必须保留查询修改逻辑的场景)

如果你因为某些原因,一定要先查用户再修改保存,那可以给保存操作加个重试逻辑——万一因为版本冲突失败了,就重新查最新的用户数据再试一次。

比如这样改:

const maxRetries = 3;
let retries = 0;
let updateSuccess = false;

while (!updateSuccess && retries < maxRetries) {
  try {
    const user = await UserModel.findById(currentUser.id);
    if (user) {
      user.photos = user.photos.filter(item => item !== id);
      await user.save();
      updateSuccess = true;
      console.log('用户更新成功');
    }
  } catch (error) {
    retries++;
    console.log(`第${retries}次重试失败:`, error.message);
    // 加个100ms延迟,避免疯狂重试
    await new Promise(resolve => setTimeout(resolve, 100));
  }
}

if (!updateSuccess) {
  throw new Error('多次重试后仍无法更新用户信息');
}

不过这种方式不如原子更新靠谱,要是并发量特别大,还是可能失败,所以优先选第一种。

3. 关闭乐观锁(非常不推荐!)

你可以在定义User模型的时候,把版本键关掉:

const userSchema = new mongoose.Schema(
  { /* 你的用户字段 */ },
  { versionKey: false } // 关闭版本锁
);

但这相当于把Mongoose给你的并发保护给扔了,要是有其他地方同时修改用户的其他字段,很容易出现数据被覆盖的问题,除非你百分百确定不会有其他并发更新,否则千万别这么干。

修改后的完整代码示例

给你把整个deletePhoto mutation改成用原子更新的版本,直接就能用:

deletePhoto: async (_root: undefined, args: { id: string }, context: UserInContext): Promise<Photo | null> => { 
  const currentUser = context.currentUser; 
  const id = args.id; 
  const isOwnPhoto = currentUser.photos.includes(id); 

  if (!currentUser || (!currentUser.isAdmin && !isOwnPhoto)) { 
    throw new AuthenticationError('Not authenticated'); 
  } 

  // 先删除图片
  const photo = await PhotoModel.findByIdAndDelete(args.id); 

  // 只有图片存在时,才更新用户的photos数组
  if (photo) {
    await UserModel.findByIdAndUpdate(
      currentUser.id,
      { $pull: { photos: id } }
    );
  }

  return photo; 
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 22:03:12