大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
前两天在技术群里看到一条消息:"生产环境订单表插入报错了,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值和类型上限的比例:
bash
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开始,必然冲突。正确做法是在迁移完成后立即同步:
bash
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 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~