侧边栏壁纸
  • 累计撰写 247 篇文章
  • 累计收到 2 条评论

为了支撑千万级数据把自增ID改造为分布式雪花算法(Snowflake),全站收录与关键词排名却断崖式暴跌?排查JS精度截断致海量内链沦为404、旧ID映射缺失权重断代、深度分页游标乱序榨干抓取与双轨映射保权我踩过的几个深坑

2026-11-1 / 0 评论 / 8 阅读
AI 摘要由 AI 生成

由于将自增ID替换为分布式雪花算法后,网站收录与关键词排名急剧下降,作者通过排查发现主要原因是JavaScript精度截断导致海量内链成为404错误,旧ID映射权重缺失以及深度分页游标乱序等问题。作者总结了在这次事故中踩过的几个深坑,包括前端JavaScript大数精度溢出、分页逻辑错误等,并提出了相应的解决方案。

上个月把核心内容表从单表自增 ID 切换成分布式雪花算法(Snowflake)之后,本以为架构改造告一段落。谁知道上线才到第三天,早晨打开搜索资源平台后台,收录量曲线直接出现断崖式下跌,索引量跌脱了近四成。同时 Nginx 日志里的 404 报警邮件每五分钟就弹出一封,百度和谷歌两家蜘蛛的抓取频次被几万个莫名其妙的死链全部吃空。

当时业务功能跑得很平稳,用户发帖、点赞、浏览都没有报过错。偏偏搜索引擎把站点打成了筛子,核心词排名跟着一路滑坡。连续通宵排查了两天,才把从前端 JavaScript 浮点数截断、旧链接权重断代,到分页游标导致蜘蛛陷入空转的几个连锁反应彻底理顺。

事故现场:搜索后台报警与异常的 404 洪峰

最先拉响警报的是搜索资源平台的死链检测工具。原本每日平稳在几百次的 404 响应,在周三凌晨突然暴涨到了三万多条,与此同时爬虫平均抓取耗时翻了近三倍。

翻出 Nginx 的 access.log,过滤出爬虫请求看了一眼:

zcat /var/log/nginx/access.log-20261101.gz | grep -E "Baiduspider|Googlebot" | grep " 404 " | head -n 10

终端刷出来的记录全长这样:

220.181.108.95 - - [01/Nov/2026:03:14:22 +0800] "GET /article/1852938472910485500.html HTTP/1.1" 404 162 "-" "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"
66.249.66.1 - - [01/Nov/2026:03:14:25 +0800] "GET /article/1852938472910485500.html HTTP/1.1" 404 162 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
220.181.108.96 - - [01/Nov/2026:03:14:29 +0800] "GET /article/1852938472910486000.html HTTP/1.1" 404 162 "-" "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"

仔细看 URL 里的数字,所有报错文章 ID 的末尾三位,要么全是 0,要么就是偶数被对齐了。

但在 MySQL 生产库里敲了一条查询:

SELECT id, title, created_at FROM t_article WHERE id LIKE '18529384729104855%' LIMIT 5;

查出来的实际主键分明是:

+---------------------+-------------------------------+---------------------+
| id                  | title                         | created_at          |
+---------------------+-------------------------------+---------------------+
| 1852938472910485507 | 分布式缓存设计与容量估算实践    | 2026-10-30 14:20:11 |
| 1852938472910485508 | 高并发场景下的限流熔断配置复盘  | 2026-10-30 14:20:12 |
| 1852938472910485514 | 微服务日志链路追踪实操记录     | 2026-10-30 14:20:15 |
+---------------------+-------------------------------+---------------------+

数据库里存的是 1852938472910485507,爬虫抓取的却是 1852938472910485500。末尾的 07 活生生变成了 00。

深坑一:JavaScript 大数精度溢出,前端给爬虫吐了海量假内链

根因其实是前端生态里一个经典的坑:IEEE 754 双精度浮点数的安全边界。

为了给后续的分库分表做铺垫,我们将原本的 BIGINT AUTO_INCREMENT 替换成了 64 位雪花算法(Snowflake)。雪花 ID 在后端 Java/Go 实体类里是一个 64 位整型(Long 或 int64),转换成十进制通常是 19 位数字。

后端通过 RESTful 接口把 JSON 数据扔给前端时,返回的结构是这样的:

{
  "code": 0,
  "data": {
    "id": 1852938472910485507,
    "title": "分布式缓存设计与容量估算实践"
  }
}

问题出在浏览器和 Node.js 服务端渲染(SSR)解析这段 JSON 的时刻。

JavaScript 里的所有数字底层都是遵循 IEEE 754 标准的双精度 64 位浮点数,它能精确表示的最大安全整数是:

console.log(Number.MAX_SAFE_INTEGER);
// 输出: 9007199254740991 (共 16 位)

当雪花算法生成的 19 位数字传入 JSON.parse() 时,超过 16 位的低位部分直接被丢失并发生了四舍五入舍入:

const raw = '{"id": 1852938472910485507}';
const obj = JSON.parse(raw);
console.log(obj.id);
// 打印结果: 1852938472910485500

前端不管是客户端 Vue/React 水合渲染,还是 Node.js SSR 生成 HTML,在模板里拼内链时:

<a href="/article/1852938472910485500.html">分布式缓存设计与容量估算实践</a>

页面虽然把标题展示出来了,底下的链接却全是被精度截断后的错乱地址。普通用户在当前页面顺着点过去,可能直接报错跳到了 404;而爬虫跟着页面上的 DOM 树遍历内链,顺藤摸瓜抓出了成千上万个根本不存在的伪造链接。

每一篇新文章的内链,都变成了带毒的死链发射器。搜索引擎的抓取配额在短短两天内被这些无效链接刷爆,正常的页面轮询被挤出队列,权重自然断崖式腰斩。

修复方法:全局序列化将 64 位整型强制转为字符串

修复的核心思路是在 API 边界将 Long 类型强制序列化为 String,绝不把 19 位的裸数字交给客户端的 JSON 解释器。

如果后端使用 Spring Boot(Jackson):

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        SimpleModule simpleModule = new SimpleModule();
        // 将所有 Long 和 long 序列化为字符串
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        return objectMapper;
    }
}

或者在具体实体类的 ID 字段上单独指定:

public class ArticleVO {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    private String title;
    // ... getter setter
}

如果后端使用 Go 语言,在结构体的 json tag 加上 ,string:

type ArticleResponse struct {
    ID    int64  `json:"id,string"`
    Title string `json:"title"`
}

如果是 Node.js BFF 中间层,遇到外部直接返回的数字类型,必须改用 json-bigint 进行反序列化:

const JSONBig = require('json-bigint')({ storeAsString: true });
const parsedData = JSONBig.parse(rawHttpResponseBody);

把 ID 全面改成带双引号的 "1852938472910485507" 之后,前端渲染出的 HTML 链接恢复了真实地址,切断了死链的持续生成。

深坑二:旧自增 ID 与新雪花 ID 割裂,历史页面权重直接被清空

修完前端内链,紧接着发现了更致命的问题。

在改造成雪花算法之前,博客积累了五年的内容,所有文章 URL 都形如:

https://www.llbbs.cn/article/128456.html

新版架构上线时,研发直接将文章详情路由改为了新雪花 ID 格式:

https://www.llbbs.cn/article/1852938472910485507.html

原本以为只要旧数据平移到新表就万事大吉,完全忽略了搜索引擎索引库里存的全部是 6 位数的旧链接。

百度和谷歌爬虫带着以前积累的外链和索引记录来回访时,旧 URL 访问全部命中后端的 SELECT * FROM t_article WHERE id = ?。雪花 ID 表里根本没有 128456 这个主键,后端统统返回 404。

五年来积累的几十万个老页面在几天之内被搜索引擎判定为内容失效,整站历史权重近乎归零。

甚至有团队为了快速止血,在 Nginx 里写了一条重定向规避 404:

# 极其危险的操作:切勿将失效内页统一 302 弹到首页
rewrite ^/article/([0-9]{1,8})\.html$ / permanent;

这种做法会被搜索引擎判定为软 404(Soft 404)或者故意作弊。旧页面的相关性和关键词权重根本不会转移到首页,反而会招致整站降权惩罚。

修复方法:Redis 建立双轨映射表,执行精准确定性 301 重定向

页面 URL 变更,唯一合规的 SEO 权重继承方式是针对每个具体页面的确定性 301(Moved Permanently)。

我们通过两步完成平滑过度:

第一步,在数据迁移时保留旧自增 ID 与新雪花 ID 的对照关系,并在 Redis 里建立快速查表缓存:

# Redis Hash 缓存:旧 ID -> 新雪花 ID
HSET article:old_to_snowflake 128456 1852938472910485507

第二步,在网关层或业务层识别 ID 特征。因为旧自增 ID 长度通常小于等于 8 位,而雪花 ID 为 19 位,可以快速分流:

# 路由拦截逻辑示例
@app.route('/article/<article_id>.html')
def get_article(article_id):
    # 如果是旧版自增 ID(长度小于 10 位)
    if len(article_id) < 10:
        new_snowflake_id = redis_client.hget('article:old_to_snowflake', article_id)
        if new_snowflake_id:
            # 严格使用 301 永久重定向到新 URL
            return redirect(f"/article/{new_snowflake_id}.html", code=301)
        return render_404()

    # 新雪花 ID 正常查库渲染
    article = db_find_article(article_id)
    if not article:
        return render_404()
    return render_template('article.html', article=article)

第三步,在新页面的 <head> 区域显式声明唯一的 Canonical 规范标签:

<link rel="canonical" href="https://www.llbbs.cn/article/1852938472910485507.html" />

当爬虫顺着旧链接抓取,收到 301 响应并在新地址里拿到 Canonical 规范标签后,搜索引擎会自动把旧 URL 的历史排名、索引评级和外链权重转移给新的雪花 URL,老页面流量才止住了下滑。

深坑三:雪花 ID 时间戳高位带来的分页游标乱序,蜘蛛遍历陷入空转

把内链和 301 解决之后,日志里的爬虫抓取频次依然不理想,而且服务器 CPU 隔一阵子就会被几个突发的慢查询打满。

排查日志发现,爬虫在抓取全站文章归档分页时,卡在了深翻页上。

原来的自增主键是单调递增的整数,全站 Sitemap 和文章分页脚本可以直接按主键递增遍历:

-- 以前基于自增 ID 的极速游标分页
SELECT id, title FROM t_article WHERE id > 128000 ORDER BY id ASC LIMIT 100;

换成分布式雪花算法后,这个简单的逻辑崩了。

雪花算法的 64 位由这几部分拼接组成:

  • 1 位符号位(固定为 0)
  • 41 位时间戳毫秒数
  • 10 位工作机器与数据中心 ID(Worker ID)
  • 12 位自增序列号

在单机低并发下,雪花 ID 大致随着时间递增;但在分布式多节点部署时,由于不同机器的时钟微小偏差和 Worker ID 分布不同,同一毫秒内不同节点生成的 ID 大小并不是绝对线性的。

更严重的是,之前为了图省事,分页逻辑在改造后退化成了传统的 OFFSET 分页:

SELECT id, title FROM t_article ORDER BY id DESC LIMIT 20 OFFSET 80000;

当百度蜘蛛尝试顺着分页链接抓取第 4000 页时,MySQL 需要在聚簇索引树上跳过前 80000 条记录再返回 20 条,执行耗时直接从 3ms 飙升到了 1.8 秒。

蜘蛛并发抓取连续发起几十个深翻页请求,连接池瞬间被耗尽,后续正常页面的抓取请求直接超时报 504 错误。爬虫多次遇到超时之后,就会自动降低对该域名的抓取评级。

修复方法:复合索引游标分页与静态化 Sitemap 矩阵

彻底抛弃 OFFSET 分页,改为以 (created_at, id) 为基准的复合游标查询,并建立联合索引:

ALTER TABLE t_article ADD INDEX idx_created_id (created_at DESC, id DESC);

分页查询时,前端翻页请求带上上一页最后一条记录的时间戳和 ID:

-- 高性能复合游标分页,利用联合索引实现常数级检索
SELECT id, title, created_at 
FROM t_article 
WHERE (created_at < '2026-10-30 14:20:11') 
   OR (created_at = '2026-10-30 14:20:11' AND id < 1852938472910485507)
ORDER BY created_at DESC, id DESC 
LIMIT 20;

同时对全站 Sitemap 进行静态化重构。不再由动态程序实时查库返回 XML,而是写一个夜间离线脚本,将存量文章按月切分为静态的 XML 文件(如 sitemap-2026-10.xml),主索引只维护 sitemap_index.xml。

当天发布的新文章,通过搜索引擎的 API(如百度资源平台主动推送接口)实时推送 URL,彻底避免爬虫在动态列表的深翻页里耗尽配额。

深坑四:Nginx 兜底拦截与死链垃圾收敛

修复完成之后,此前被截断的那批尾号为 00 的错误 URL 已经在互联网缓存中扩散了一部分,外链和爬虫调度池短时间内依然会偶尔请求这些失效链接。

如果直接让它们穿透到后端服务,不仅消耗应用服务器资源,也会持续在搜索控制台累计错误指标。

我们在 Nginx 入口层配置了一条识别规则,针对明显带有截断特征的伪造 ID 进行阻断,并返回带有无索引标记的响应头:

# 拦截末尾带连续三个 0 且长度符合雪花算法 19 位的异常访问
location ~* ^/article/([0-9]{16}000)\.html$ {
    # 显式告知爬虫该地址永久作废且禁止索引
    add_header X-Robots-Tag "noindex, nofollow" always;
    return 404;
}

配合在百度搜索资源平台提交一次正则死链规则文件:

https://www.llbbs.cn/article/*000.html

通过平台工具引导蜘蛛主动从抓取队列中移除这批伪造的截断链接。

止血后的验证与线上收敛

全部改动上线之后,我们通过终端用不同的爬虫 UA 模拟抓取,检查状态码和页面输出:

# 验证旧自增 ID 是否返回确定性 301
curl -I -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" \
  "https://www.llbbs.cn/article/128456.html"

# 预期输出:
# HTTP/1.1 301 Moved Permanently
# Location: https://www.llbbs.cn/article/1852938472910485507.html

# 验证新雪花 URL 的 Canonical 标签与正文内链是否为字符串
curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
  "https://www.llbbs.cn/article/1852938472910485507.html" | grep -E "canonical|href=\"/article/" | head -n 5

核对响应结果,新页面的 href 链接中没有再出现末尾三位归零的现象,Canonical 标签指向完整规范地址。

改造上线两周后,搜索资源平台的死链报警彻底平息,404 错误率从 38% 下降到了常规的 0.05% 以下。爬虫平均抓取耗时稳定在 120ms 左右,索引收录量也逐步回升至事故发生前的水准。分布式 ID 改造是技术架构演进的一大步,但如果忽略了网络协议层和浏览器运行时的细节约束,看似不起眼的几位数字溢出,也足以在搜索引擎收录层面引发一场雪崩。

评论一下?

OωO
取消