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

Next.js API Promise异常:快速点击致点赞计数错误求助

解决Next.js点赞功能快速点击导致计数重复累加的问题

问题现象

快速点击点赞按钮时,并发请求引发竞态条件,导致数据库出现重复插入报错,点赞计数异常翻倍。

核心原因分析

  • 前端无请求限制:快速点击发起多个并发异步请求,前一个请求未完成时后续请求已发送,后端处理逻辑被打乱
  • 前端状态更新错误:直接修改状态对象属性(如setNew[key].youLike)违反React状态的immutable原则,且未使用函数式更新,易导致状态不同步
  • 后端操作非原子性:先查询youLike、再执行插入/删除、最后更新计数的三步操作并非原子操作,并发时多个请求会同时读到"未点赞"状态,进而重复执行插入
  • 数据库无唯一约束:cms_likes表未对owner_id+type+post_id设置联合唯一约束,无法从底层阻止重复数据插入
  • SQL注入风险:通过字符串拼接生成SQL语句,存在安全隐患
  • 数据库连接管理不当:executeQuery每次执行后关闭连接,频繁创建/销毁连接会引发性能问题与连接异常

解决方案

前端优化

1. 增加请求控制(防抖/按钮禁用)

通过状态控制按钮可用性,确保同一时间仅处理一个请求:

import { useState } from 'react';
import axios from 'axios';

const LikeButton = ({ key, data, initialState }) => {
  const [isProcessing, setIsProcessing] = useState(false);
  const [likeState, setLikeState] = useState(initialState);

  const handleLike = async () => {
    if (isProcessing) return;
    setIsProcessing(true);
    
    try {
      const response = await axios.post("/api/reaction", data);
      // 使用函数式更新确保基于最新状态修改
      setLikeState(prev => ({
        ...prev,
        youLike: response.data.react,
        likes: response.data.likes
      }));
    } catch (err) {
      console.error('点赞操作失败:', err);
    } finally {
      setIsProcessing(false);
    }
  };

  return (
    <button onClick={handleLike} disabled={isProcessing}>
      {isProcessing ? '处理中...' : (likeState.youLike ? '取消点赞' : '点赞')}
      ({likeState.likes})
    </button>
  );
};

2. 修正状态更新逻辑

禁止直接修改状态对象,使用React函数式更新保证状态一致性:

// 错误写法
// setNew[key].youLike = true;
// setting(setNew => [...setNew]);

// 正确写法
setting(prevState => {
  const updatedState = [...prevState];
  updatedState[key] = {
    ...updatedState[key],
    youLike: response.data.react,
    likes: response.data.likes
  };
  return updatedState;
});

后端优化

1. 数据库添加联合唯一约束

先在数据库层面阻止重复插入:

ALTER TABLE cms_likes ADD UNIQUE INDEX idx_unique_like (owner_id, type, post_id);

2. 原子化数据库操作

使用事务+行锁保证操作的原子性,避免并发冲突:

export default async function react(req, res) {
  let msg, msg_type, react, likes;
  const { type, id } = req.body;
  const myId = mySession(req);

  if (!myId || !type || !id) {
    return res.send({
      msg: "请求参数错误",
      msg_type: "error",
      react: null,
      likes: null
    });
  }

  try {
    // 开启事务
    await db.beginTransaction();

    // 查询并加行锁,防止并发修改
    const likeRecord = await db.query(
      "SELECT * FROM cms_likes WHERE owner_id = ? AND type = ? AND post_id = ? FOR UPDATE",
      [myId, type, id]
    );
    react = likeRecord.length > 0;

    if (react) {
      // 取消点赞
      await db.query(
        "DELETE FROM cms_likes WHERE owner_id = ? AND type = ? AND post_id = ?",
        [myId, type, id]
      );
      await db.query(
        "UPDATE cms_posts SET likes = likes - 1 WHERE id = ?",
        [id]
      );
      react = false;
    } else {
      // 添加点赞
      await db.query(
        "INSERT INTO cms_likes (owner_id, type, post_id, time) VALUES (?, ?, ?, ?)",
        [myId, type, id, timeNow("s")]
      );
      await db.query(
        "UPDATE cms_posts SET likes = likes + 1 WHERE id = ?",
        [id]
      );
      react = true;
    }

    // 获取最新点赞数
    const likesResult = await db.query(
      "SELECT likes FROM cms_posts WHERE id = ?",
      [id]
    );
    likes = likesResult[0]?.likes;

    // 提交事务
    await db.commit();
  } catch (err) {
    // 回滚事务
    await db.rollback();
    msg_type = "error";
    msg = "操作失败";
    console.error(err);
  }

  res.send({ msg, msg_type, react, likes });
}

3. 修复SQL注入问题

全程使用参数化查询替代字符串拼接,避免安全风险。

4. 优化数据库连接管理

取消每次查询后关闭连接的操作,改用连接池管理连接:

export default async function executeQuery({ query, values }) {
  try {
    const results = await db.query(query, values);
    // 移除db.end(),连接由连接池自动管理
    return results;
  } catch (err) {
    throw err; // 抛出错误交由上层事务处理
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 08:45:30