刷新后点赞全变 0?别急着怪 Redis,这八成是 Long 被 JavaScript 偷偷“改号”了(一次线上复盘)

作者:WanderInk日期:2025/12/25

做社区功能的人,多半都经历过这种抓狂时刻:你在帖子上点了个赞,按钮立刻高亮,数字也加一,用户体验看起来很丝滑;可你一刷新页面,点赞数像被人清空了一样,全部回到 0。你打开 Redis 客户端看,计数 key 明明存在,值也不是 0。于是你开始怀疑缓存一致性,怀疑是不是读了另一台 Redis,怀疑线上 jar 没更新,甚至怀疑自己是不是在梦里写代码。

ChatGPT Image 2025年12月26日 00_06_20.png

我得说,这类问题最阴的地方就在于它特别像缓存问题,实际上却往往跟缓存一点关系都没有。真正的凶手是数据类型边界,准确地说,是 Java 的 long 雪花 id 进了 JavaScript 的 Number 世界以后超过安全整数范围,发生了精度丢失,导致你点赞写入用的是一个“被四舍五入后的 id”,刷新读取用的又是“真实 id”。写入和读取压根不是同一个目标,你换十台 Redis 也救不了。

我当时定位这个问题的切入点很朴素,不靠猜,全靠证据。把浏览器 Network 打开,盯住两处就够了:列表或者详情接口返回的帖子 id,和点赞接口请求路径里带的 id。正常情况下它们应该一模一样,哪怕多一个字符都不行。结果我看到的非常刺眼,点赞请求打的是 .../posts/2003003909205840000/like,列表返回的真实 id 却是 2003003909205839874。你看这俩数字差得不多,甚至肉眼一滑可能以为一样,但在业务上它们就是两个完全不同的帖子。更关键的是,这种“差一点点”的差法特别典型,末尾被抹平,被凑整,被改号,这是 JavaScript 精度丢失的指纹。

ChatGPT Image 2025年12月26日 00_06_50.png

原因在于 JavaScript 的 Number 是双精度浮点数,安全整数上限是 2^53-1,也就是 9007199254740991。雪花 id 动辄十八十九位,早就远远超过这个范围。超过之后,JS 不是直接报错,它会用一个最接近的可表示值来存,于是你以为你拿到了 2003003909205839874,实际上它在内存里已经变成了另一个相邻但不同的数。前端把这个“变形 id”拼进 URL 发给后端,后端又是强类型世界,收到什么就当什么,于是 Redis 的 key 写成 forum:like:count:1:2003003909205840000,数据库里 target_id 也可能跟着写错。刷新页面时,列表接口从数据库读出真实帖子,再把真实 id 返回给前端,前端用真实 id 去查计数,当然查不到,因为计数全在另一个 id 上,于是你看到的就是“刷新后全是 0”。这不是缓存不一致,这是写错对象了,属于逻辑层面彻底错位。

这种坑之所以容易把人带沟里,是因为点赞当下看起来“生效”。很多实现会在点击后先做乐观更新,UI 先加一,给用户即时反馈。再加上 Redis 里确实有 key 有值,你就更容易误判成“读取链路有问题”。但只要你把写入 id 和读取 id 对齐看一眼,真相就很干净,干净得甚至有点残忍。

修复这类问题,我的经验是别在前端做花活,工程上最稳的路线是后端把所有 Long 统一序列化成字符串,让 id 从一开始就别进 JS 的 Number 世界。只要 JSON 里 id 带引号,前端拿到的就是 string,拼 URL 比较相等都不会丢精度。Spring Boot 里用 Jackson 做全局配置就行,把 Long 和 long 都用 ToStringSerializer 输出,类似这样:

1package com.meme.generator.config;
2
3import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
4import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
5import org.springframework.context.annotation.Bean;
6import org.springframework.context.annotation.Configuration;
7
8@Configuration
9public class JacksonConfig {
10
11    @Bean
12    public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() {
13        return builder -> {
14            builder.serializerByType(Long.class, ToStringSerializer.instance);
15            builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
16        };
17    }
18}
19

上线之后你会很直观地看到变化,接口返回从 id: 200300... 变成 "id":"200300..."。这一步就是治本,因为你把风险源头掐掉了。前端这边也别犹豫,所有 id 一律当字符串处理,路由参数、请求参数、Map key、对比逻辑都用 string,别手贱去 Number(id)。如果你用 TypeScript,把 DTO 里的 id 类型直接写死成 string,能提前拦住一堆无意的隐式转换。

光把 Long 转字符串还不够,我习惯再加一道保险,防止系统继续被脏请求污染。点赞接口在真正写 Redis 和写 DB 之前,先校验 targetId 必须真实存在。原因很现实,就算你前端全修好了,也总会有人抓包乱打接口,或者旧版本前端还在外面跑,甚至某些数据链路中间又把 id 搞成了 number。你不想 Redis 和点赞表里永远躺着一堆“幽灵帖子”的点赞记录,最好的办法就是入口处把门关死,不存在的帖子或评论直接拒绝,别写入任何东西。这样系统的容错会强很多,你以后做统计、排行、消息通知也不至于被脏数据恶心。

修复上线以后验证也很简单,不需要写什么复杂用例。你打开 Network 看一眼列表接口响应,只要 id 带引号,基本可以确认你已经跨过了 JS 精度这个坑。如果你发现线上还是数字 id,那就别往下测了,说明你压根没部署到新版本或者配置没生效。接着随便点个赞,观察点赞请求路径里的 id 是否和列表返回完全一致,然后刷新页面看 likeCount 是否还能保持一致。如果这三件事都稳了,这个问题就可以宣布结束,属于“永不复发”那一类。

最后别忘了历史脏数据。这个坑一旦发生过,你 Redis 里很可能已经存在一批错误 id 的计数 key,数据库里也可能已经有 target_id 指向不存在目标的点赞记录。新版本不会再产生,但旧的如果不清理,迟早会在某个统计功能里反咬你一口。清理的思路也不复杂,本质上就是删掉所有指向不存在目标的点赞记录。举个常见场景,如果你的点赞表是 forum_like,帖子表是 forum_post,并且 target_type=1 代表帖子,你可以在确认备份和测试之后用类似的 SQL 找并删除“不存在帖子”的点赞记录:

1DELETE fl
2FROM forum_like fl
3LEFT JOIN forum_post fp ON fp.id = fl.target_id
4WHERE fl.target_type = 1
5  AND fp.id IS NULL;
6

Redis 清理也建议用 SCAN 去做渐进删除,别在生产上用 KEYS 图省事,Redis 这玩意你真把它阻塞了,后果比点赞错位严重多了。

写到这里你会发现,这次事故真正的教训其实很工程化:跨语言系统里,id 这种一旦出错就会写错对象的字段,永远不要把它当数值去传。尤其是雪花 long,在 Java 世界里再自然不过,到了 JS 世界里就是个随时可能变形的“危险品”。把 Long 输出成字符串不是洁癖,是对线上稳定性的尊重。

ChatGPT Image 2025年12月26日 00_07_25.png

如果你现在也被“刷新后点赞全是 0”折磨着,我建议你先别急着调 Redis 和部署链路,先做一件最便宜但最有效的事:对比一下列表返回的 id 和点赞请求里的 id 是否完全一致。只要它们差一点点,你就已经抓到凶手了。把 Long 全局转字符串,再把点赞入口加上存在性校验,最后清掉历史脏数据,你会发现这个问题结束得非常干脆,甚至有点爽。


刷新后点赞全变 0?别急着怪 Redis,这八成是 Long 被 JavaScript 偷偷“改号”了(一次线上复盘)》 是转载文章,点击查看原文


相关推荐


【金猿人物展】涛思数据创始人、CEO陶建辉:实现AI时代时序数据库向“数据平台”的转型
数据猿2025/12/16

陶建辉 “【提示】2025第八届年度金猿颁奖典礼将在上海举行,此次榜单/奖项的评选依然会进行初审、公审、终审三轮严格评定,并会在国内外渠道大规模发布传播欢迎申报。 大数据产业创新服务媒体 ——聚焦数据 · 改变商业 在数字化转型与AI技术爆发的浪潮中,时序数据库作为处理海量实时数据的核心工具,已成为工业互联网、自动驾驶、能源电力等领域的刚需。 作为国内时序数据库赛道的领军企业,涛思数据从2016年入局至今,凭借精准的赛道选择、持续的技术迭代与独特的发展策略,实现了从单一产品到生


深度学习在教育数据挖掘(EDM)中的方法体系:从任务建模到算法范式的理论梳理与总结
智算菩萨2025/12/8

目录 1 引言 2 理论知识与技术基础 2.1 教育数据的形式化:事件流、序列、图与稀疏矩阵 2.2 监督学习的目标函数:从分类到排序 2.3 表示学习与自编码器:从重构到迁移 2.4 图神经网络与知识图谱:结构归纳偏置 2.5 生成模型与能量函数:RBM/DBN 的另一条线 2.6 强化学习:把推荐与学习路径当作序列决策 3 EDM 的典型任务与场景:问题定义、输入输出与评价方式 4 深度学习范式在 EDM 中的总体框架:监督、无监督与强化学习如何落到教育任务 4.1 监


一文搞懂 Webpack 分包:async、initial 与 all 的区别【附源码】
程序员大卫2025/11/28

大家好,我是前端架构师,关注微信公众号【程序员大卫】免费领取精品资料。 1. 背景 最近在优化一个项目的加载性能时,对 optimization.splitChunks.chunks 的三个可选值 async、initial 和 all 的具体效果产生了疑惑。为了彻底搞清楚它们的区别,我专门搭建了一个 Demo 进行对比研究。 2. 核心区别:async vs initial chunks 属性决定了 Webpack 对哪些类型的代码块进行分割。其中 async 是默认配置。 经过测试发现:在单


GDAL 实现自定义数据坐标系
GIS之路2026/1/3

^ 关注我,带你一起学GIS ^ 前言 ❝ 在GIS开发中,经常需要进行数据的转换处理,特别是Shapefile数据的投影转换更是重中之重,如何高效、准确的将源数据坐标系转换到目标坐标系是我们需要研究解决的问题。 在之前的文章中讲了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,本篇教程在之前一系列文章的基础上讲解如何使用GDAL实现自定义数据坐标系。 GDAL 简介 GDAL 下载安装 GDAL 开发起步 GDAL 实现 GIS 数据读取转换(全) 如


hive问题
心止水j2026/1/11

一、基础概念 问题:简述 Hive 的定义及核心作用?答案:Hive 是基于 Hadoop 的数据仓库工具,支持类 SQL(HQL)查询分析;核心作用是让非开发人员通过 SQL 分析 Hadoop 上的海量数据。 问题:Hive 的元数据默认存储在哪里?生产环境中为什么要替换为 MySQL?答案:默认存 Derby;生产换 MySQL 因 Derby 仅单用户、不支持并发,MySQL 支持多用户并发、数据持久化且易维护。 问题:Hive 支持哪些执行引擎?它们的区别是什么?答案


2025.12.17华为软开
ゞ 正在缓冲99%…2026/1/19

细胞增殖 import java.util.HashMap; import java.util.Map; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); long n = scanner.nextLong();//n个观测值

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客