自增主键用尽了怎么办?INT溢出、在线迁移与预防策略全解析

0 阅读5分钟

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

前两天在技术群里看到一条消息:“生产环境订单表插入报错了,Duplicate entry '2147483647' for key 'PRIMARY'。”

这是典型的INT自增主键溢出

在展开之前,先把几个关键概念说清楚。

什么是自增主键? 主键是表中唯一标识每一行记录的字段。自增主键(AUTO_INCREMENT)是MySQL的一种机制——每次插入新记录时,数据库自动为这个字段分配一个递增的整数值,不需要应用层手动指定。好处是简单、高效、天然保证唯一性。

什么是主键溢出? 每种数据类型都有取值上限。INT类型用4个字节存储,有符号时最大值为2,147,483,647(约21亿),无符号时最大值为4,294,967,295(约42亿)。当自增主键的值达到这个上限后,InnoDB内部的计数器不会自动循环,后续INSERT操作会持续触发ER_DUP_ENTRY错误,导致整库写入停服

为什么这个问题越来越常见? 很多团队在项目初期用INT做主键,认为“21亿够用了”。但对于日均写入百万行的订单表、日志表、流水表,21亿在几年内就可能触达。更关键的是,很多团队没有监控自增ID消耗进度的习惯,等到报错了才发现——而这时候留给迁移准备的时间窗口可能只有数周。

一、怎么提前发现?

在问题爆发之前,可以主动监控ID消耗进度。通过查询information_schema,可以精准获取各表的AUTO_INCREMENT值和类型上限的比例:

SELECT 
    t.TABLE_SCHEMA,
    t.TABLE_NAME,
    c.DATA_TYPE,
    t.AUTO_INCREMENT,
    CASE 
        WHEN c.DATA_TYPE = 'int' AND c.COLUMN_TYPE NOT LIKE '%unsigned%' THEN 2147483647
        WHEN c.DATA_TYPE = 'int' AND c.COLUMN_TYPE LIKE '%unsigned%' THEN 4294967295
        WHEN c.DATA_TYPE = 'bigint' THEN 9223372036854775807
    END AS MAX_VALUE,
    (t.AUTO_INCREMENT / CASE 
        WHEN c.DATA_TYPE = 'int' AND c.COLUMN_TYPE NOT LIKE '%unsigned%' THEN 2147483647
        WHEN c.DATA_TYPE = 'int' AND c.COLUMN_TYPE LIKE '%unsigned%' THEN 4294967295
        ELSE 9223372036854775807
    END) * 100 AS usage_ratio
FROM information_schema.TABLES t
JOIN information_schema.COLUMNS c 
    ON t.TABLE_SCHEMA = c.TABLE_SCHEMA 
    AND t.TABLE_NAME = c.TABLE_NAME
WHERE c.EXTRA = 'auto_increment'
    AND t.AUTO_INCREMENT IS NOT NULL
HAVING usage_ratio > 80;

监控阈值建议:80%触发预警,90%触发紧急告警。在海量写入场景下,从90%到100%的窗口期可能只有数周,留给迁移准备的时间非常有限。

二、在线迁移方案:INT → BIGINT

最直接的方案是把自增主键从INT升级为BIGINT。BIGINT占用8字节,有符号上限约922亿亿,足够绝大多数业务使用。

不能直接执行ALTER TABLE。修改数值类型会触发MySQL重建整张表,锁表时间与数据量正相关。几百GB的表执行ALTER TABLE t MODIFY id BIGINT,可能持续数小时,期间写入完全阻塞。即使MySQL 5.6+支持ALGORITHM=INPLACE,修改数值类型也不支持原地升级。

生产环境需要用gh-ost做无锁迁移。核心流程是:创建影子表 → 复制存量数据 → 通过Binlog增量同步 → 原子切换。

有几个容易踩的坑需要注意:

坑1:AUTO_INCREMENT值不会自动继承。 gh-ost新建的影子表初始AUTO_INCREMENT=1。如果不处理,切流后新插入数据会从1开始,必然冲突。正确做法是在迁移完成后立即同步:

ALTER TABLE _t_gho AUTO_INCREMENT = (
    SELECT AUTO_INCREMENT FROM information_schema.TABLES 
    WHERE TABLE_SCHEMA='db' AND TABLE_NAME='t'
);

坑2:外键关联的子表字段必须同步改为BIGINT。 忽略这一步会导致ERROR 1215

坑3:应用层代码需要同步升级。 Java应用中int类型的ID字段必须升级为long,ORM映射也需要调整。

三、更彻底的方案:分布式ID

如果团队已经在做分库分表,或者业务量增长极快,INT迁移到BIGINT可能只是“续命几年”。更彻底的方案是逐步废弃自增主键,改用分布式ID生成方案

常见方案包括:雪花算法(Snowflake)、号段模式(如美团Leaf)、以及基于数据库的全局序列。

金仓KES Sharding内置了全局序列能力,提供高性能、无冲突的全局唯一ID生成机制。对于从集中式向分布式演进的银行核心系统来说,全局序列是分布式架构的基础组件之一——应用层不需要自己维护ID生成逻辑,数据库层面统一提供。

四、预防比迁移更重要

如果你的系统还没到21亿,现在就是最好的预防时机。

新建表时直接用BIGINT。 BIGINT只比INT多占4字节存储空间,但对于避免未来的迁移成本来说,这点开销可以忽略不计。

对于已上线的INT表,把上面的监控脚本加到日常巡检中。接近80%时提前规划迁移方案,不要等到报错了才行动。

五、小结

自增主键溢出是一个“看起来很远、实际很近”的问题。INT的21亿上限,对于日均百万写入的系统,可能就是几年的光景。提前监控、提前规划在线迁移方案、新建表直接用BIGINT——这三件事做好,就能避免被一个主键类型卡住整条业务线。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~