并发请求下Mongoose重复减少评论点赞数问题及修复咨询
问题描述
我用Node.js + Mongoose开发博客API,在实现评论点赞/点踩功能时遇到并发问题:当用户连续触发两次点赞请求(比如快速点击两次按钮),功能逻辑异常。比如连续点赞时,现有逻辑本应是"已点赞则取消点赞并减少点赞数",但两次并发请求会导致点赞数被减少两次。
现有点赞功能代码:
exports.upvoteComment = async (req, res, next) => { const { id } = req.params const comment = await Comment.findById(id) if (!comment) { return next(new AppError("Comment not found", 404)) } const vote = await Vote.findOneAndUpdate({ type: 'comment', refId: comment.id, authorId: req.user.id }, { type: 'comment', refId: comment.id, upvoted: true, authorId: req.user.id }, { upsert: true }) if (!vote || !vote.upvoted) { comment.upvotes = comment.upvotes + 1; } if (vote && vote.upvoted) { comment.upvotes = comment.upvotes - 1; await comment.save() await vote.remove() return res.status(200).json({ ok: true, voted: false, upvoted: false, upvotes: comment.upvotes, downvotes: comment.downvotes }) } if (vote && !vote.upvoted) { comment.downvotes = comment.downvotes - 1; } await comment.save() return res.status(200).json({ ok: true, voted: true, upvoted: true, upvotes: comment.upvotes, downvotes: comment.downvotes }) }
Vote模型定义:
{ type: { type: String, required: true, enum: ["blog", "comment"] }, upvoted: { type: Boolean, required: true, }, refId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true }, authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true } }
问题根源
- 竞态条件:两次并发请求会同时读取到相同的
comment和vote状态,各自执行修改逻辑,导致最终计数错误。比如两次请求都读到"已点赞"状态,都会执行comment.upvotes -=1,最终点赞数被减两次。 - 非原子操作:现有逻辑中,查询Vote、修改Comment计数、保存Comment这几步是分开的,没有用原子操作或事务保证一致性。
findOneAndUpdate返回值逻辑漏洞:{upsert: true}默认返回旧文档,若新创建Vote则返回null,后续逻辑依赖此返回值判断操作类型,容易触发错误分支。
修复方案
1. 给Vote模型添加唯一索引
在Vote模型的Schema中添加复合唯一索引,从数据库层面限制同一用户对同一评论只能存在一条投票记录:
// Vote模型Schema定义中添加 VoteSchema.index({ type: 1, refId: 1, authorId: 1 }, { unique: true });
2. 使用事务+原子更新保证一致性
将Vote的增删改和Comment的计数修改放在同一事务中,同时用Mongoose的$inc原子操作更新计数,彻底避免竞态条件:
修改后的完整代码:
exports.upvoteComment = async (req, res, next) => { const session = await mongoose.startSession(); session.startTransaction(); try { const { id } = req.params; const userId = req.user.id; // 事务内查找评论 const comment = await Comment.findById(id).session(session); if (!comment) { await session.abortTransaction(); session.endSession(); return next(new AppError("Comment not found", 404)); } // 查询用户现有投票 const existingVote = await Vote.findOne({ type: 'comment', refId: id, authorId: userId }).session(session); let responseData; if (existingVote) { if (existingVote.upvoted) { // 取消点赞:删除投票,点赞数减1 await Vote.findByIdAndDelete(existingVote._id).session(session); await Comment.findByIdAndUpdate( id, { $inc: { upvotes: -1 } }, { session, new: true } ); responseData = { ok: true, voted: false, upvoted: false, upvotes: comment.upvotes - 1, downvotes: comment.downvotes }; } else { // 从点踩改为点赞:更新投票,点踩数减1、点赞数加1 await Vote.findByIdAndUpdate( existingVote._id, { upvoted: true }, { session } ); await Comment.findByIdAndUpdate( id, { $inc: { downvotes: -1, upvotes: 1 } }, { session, new: true } ); responseData = { ok: true, voted: true, upvoted: true, upvotes: comment.upvotes + 1, downvotes: comment.downvotes - 1 }; } } else { // 新增点赞:创建投票,点赞数加1 await Vote.create([{ type: 'comment', refId: id, authorId: userId, upvoted: true }], { session }); await Comment.findByIdAndUpdate( id, { $inc: { upvotes: 1 } }, { session, new: true } ); responseData = { ok: true, voted: true, upvoted: true, upvotes: comment.upvotes + 1, downvotes: comment.downvotes }; } await session.commitTransaction(); session.endSession(); return res.status(200).json(responseData); } catch (err) { await session.abortTransaction(); session.endSession(); next(err); } };
3. 前端防抖辅助优化
在前端给点赞按钮添加防抖逻辑(比如300ms内只允许一次请求),从源头减少并发请求的概率,配合后端的原子操作形成双重保障。
内容的提问来源于stack exchange,提问作者saibot-tsch
相关产品推荐
相关产品推荐

