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

并发请求下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
    }
}

问题根源

  1. 竞态条件:两次并发请求会同时读取到相同的comment和vote状态,各自执行修改逻辑,导致最终计数错误。比如两次请求都读到"已点赞"状态,都会执行comment.upvotes -=1,最终点赞数被减两次。
  2. 非原子操作:现有逻辑中,查询Vote、修改Comment计数、保存Comment这几步是分开的,没有用原子操作或事务保证一致性。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 15:39:13