Nuxt 3 点赞连点把数字打飘:我用请求去重、Redis 原子计数和失败回滚稳住状态
一次连点导致点赞数忽高忽低,我把前端乐观更新、服务端幂等和缓存失败回滚串起来,避免状态各说各话。

作者
ZHOUYI
周末看后台监控,发现一篇文章的点赞数像心电图:用户连点几下,数字先涨到 7,刷新后又变回 5。问题不复杂:前端乐观更新太快,服务端没有幂等,缓存和数据库各说各话。我把它当成一次状态一致性练习,分成前端去重、服务端原子计数、失败回滚三层处理。
前端:乐观更新可以,但别重复发请求
我保留点赞后的即时反馈,但不再允许同一用户对同一文章并发提交。点击后先检查本地是否有 pending 请求,有就只更新按钮态;同时用节流和禁用防止手抖。真正发送到服务端的,只有最后一次明确状态。
- 用 pendingMap 记录 postId + userId,请求未返回前不重发
- 取消点赞/点赞切换时,以最后一次操作为准,避免旧响应覆盖新状态
- 接口失败时先恢复 UI,再提示用户,不把错误藏在 loading 里
服务端:用 Redis 做幂等,再落 MongoDB
服务端不能信任前端次数。点赞这类操作适合用唯一键约束:一个用户对一个文章只能贡献一次。我先在 Redis 里写入 like:postId:userId,写入成功才增加计数;写失败说明重复请求,直接返回当前状态。计数用 Redis 原子操作,避免并发下读改写竞争。
const likeKey = `like:${postId}:${userId}`;
const countKey = `like:count:${postId}`;
const added = await redis.set(likeKey, '1', 'NX', 'EX', 86400);
if (!added) return getLikeCount(postId);
await redis.incr(countKey);
try {
await db.posts.updateOne({ _id: postId }, { $inc: { likes: 1 } });
} catch (err) {
await redis.decr(countKey);
await redis.del(likeKey);
throw err;
}失败回滚:别让缓存数字飘着
Redis 先加计数,MongoDB 写入失败时,必须把 Redis 扣回来,否则页面显示虚高。这和支付回调很像:快读层可以先响应,但事实源要能补齐。我会把 Redis 当快读层,不当唯一事实源;详情页优先读缓存,必要时回源 Mongo 校准。取消点赞也走同一套唯一键删除和原子递减,失败同样回滚。
“乐观更新不是少处理失败,而是把失败路径做得像成功路径一样确定。”
修完后我连续点了十几次,数字不再乱跳。这个 BUG 给我的提醒是:点赞看着小,实际是前端状态、接口幂等、缓存一致性三件事叠在一起。先把重复请求挡住,再让服务端每次操作都有唯一凭证,最后给失败留好回滚,状态才会稳。