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
相关产品推荐
相关产品推荐

