如何避免连续执行GraphQL删除图片Mutation时的竞态条件?
搞定并发删除时的用户更新失败问题
哥们,你这问题我太熟了——这是Mongoose乐观锁机制导致的并发更新冲突!咱们先掰扯清楚为啥会这样,再给你几个靠谱的解决办法。
为啥会出这问题?
你看哈,当你快速连续触发5次删除请求时:
- 所有请求几乎同时从数据库里读出来同一个版本的用户文档(比如日志里的版本116)
- 第一个请求修改完用户的
photos数组,保存成功后,用户文档的版本号自动升到117 - 后面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
相关产品推荐
相关产品推荐

