·更新于 2026年10月25日· 5 次浏览 Nuxt 3Vue 3Node.jsRedisMongoDBBUG解决工程实践

Nuxt 3 点赞连点把数字打飘:我用请求去重、Redis 原子计数和失败回滚稳住状态

一次连点导致点赞数忽高忽低,我把前端乐观更新、服务端幂等和缓存失败回滚串起来,避免状态各说各话。

Nuxt 3 点赞连点把数字打飘:我用请求去重、Redis 原子计数和失败回滚稳住状态 封面
ZHOUYI

作者

ZHOUYI

周末看后台监控,发现一篇文章的点赞数像心电图:用户连点几下,数字先涨到 7,刷新后又变回 5。问题不复杂:前端乐观更新太快,服务端没有幂等,缓存和数据库各说各话。我把它当成一次状态一致性练习,分成前端去重、服务端原子计数、失败回滚三层处理。

前端:乐观更新可以,但别重复发请求

我保留点赞后的即时反馈,但不再允许同一用户对同一文章并发提交。点击后先检查本地是否有 pending 请求,有就只更新按钮态;同时用节流和禁用防止手抖。真正发送到服务端的,只有最后一次明确状态。

  • 用 pendingMap 记录 postId + userId,请求未返回前不重发
  • 取消点赞/点赞切换时,以最后一次操作为准,避免旧响应覆盖新状态
  • 接口失败时先恢复 UI,再提示用户,不把错误藏在 loading 里

服务端:用 Redis 做幂等,再落 MongoDB

服务端不能信任前端次数。点赞这类操作适合用唯一键约束:一个用户对一个文章只能贡献一次。我先在 Redis 里写入 like:postId:userId,写入成功才增加计数;写失败说明重复请求,直接返回当前状态。计数用 Redis 原子操作,避免并发下读改写竞争。

js
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 给我的提醒是:点赞看着小,实际是前端状态、接口幂等、缓存一致性三件事叠在一起。先把重复请求挡住,再让服务端每次操作都有唯一凭证,最后给失败留好回滚,状态才会稳。

Guestbook

留言

若这篇文章也曾停在你心上,留一行字吧,我会看的。

先确认一下邮箱

多这一步,是为了少些机器骚扰,也让我能更踏实地读每一条真人写下的字。验证码 10 分钟内有效;同一邮箱 60 秒内只能发一封。

本文留言

共 0 条已加载 0 条

这里还静悄悄的——愿意做第一个留下脚印的人吗?