自增主键用完了怎么办?
结论:自增主键耗尽的表现是「插入报主键冲突」,而不是报溢出;此时表已经写不进去,唯一稳妥的出路是提前把类型换成
BIGINT UNSIGNED。所以真正的问题不是「用完了怎么办」,而是「怎么在用到一半时就发现」。
一、各整数类型的上限
| 类型 | 有符号上限 | 无符号上限 | 按每秒 1000 条估算可用时长 |
|---|---|---|---|
TINYINT | 127 | 255 | 不到 1 秒 |
SMALLINT | 32767 | 65535 | 约 1 分钟 |
MEDIUMINT | 8388607 | 16777215 | 约 4.6 小时 |
INT | 2147483647 | 4294967295 | 约 49.7 天 |
BIGINT | 9.22×10¹⁸ | 1.84×10¹⁹ | 理论上亿年 |
signed INT 撑不住日增百万的表;INT UNSIGNED 也只多一倍。高写入表从第一天就该用 BIGINT UNSIGNED。
二、耗尽时到底发生什么
id INT UNSIGNED AUTO_INCREMENT,当前最大值 4294967295
INSERT INTO t (name) VALUES ('x');
│
▼
InnoDB 申请下一个自增值 → 4294967296,超出列范围
│
▼
ERROR 1062 (23000): Duplicate entry '4294967295' for key 't.PRIMARY'
注意错误码是 1062 主键冲突而不是 1264 越界:自增分配器把值钳到列上限,于是每次都拿同一个值去插,表现成持续冲突。这也解释了为什么监控里看到的是「主键冲突暴涨」却找不到重复写入的业务代码。
三、先量化还剩多少
-- 当前自增水位与类型上限的比值
SELECT
table_name,
auto_increment,
CASE data_type
WHEN 'int' THEN IF(column_type LIKE '%unsigned%', 4294967295, 2147483647)
WHEN 'bigint' THEN IF(column_type LIKE '%unsigned%', 18446744073709551615, 9223372036854775807)
END AS max_value,
ROUND(auto_increment / CASE data_type
WHEN 'int' THEN IF(column_type LIKE '%unsigned%', 4294967295, 2147483647)
ELSE 9223372036854775807 END * 100, 4) AS used_pct
FROM information_schema.tables t
JOIN information_schema.columns c
ON c.table_schema = t.table_schema
AND c.table_name = t.table_name
AND c.column_key = 'PRI'
WHERE t.table_schema = 'demo' AND t.auto_increment IS NOT NULL;
把 used_pct 接到监控上,超过 60% 就排期改造,比事后救火便宜一个数量级。
四、改类型:能在线做,但要排期
-- 8.0 中修改列类型需要重建表,务必显式声明 INPLACE + 允许并发 DML
ALTER TABLE t_order
MODIFY id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
ALGORITHM = INPLACE, LOCK = NONE;
-- 执行前确认没有长事务阻塞元数据锁
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id
FROM information_schema.innodb_trx
ORDER BY trx_started;
千万级以上的表建议用 gh-ost 或 pt-online-schema-change 做影子表迁移:先建 BIGINT 的新表、持续追平增量、最后原子改名。直接 ALTER 也支持并发 DML,但会在开始与结束各取一次排他元数据锁,被长事务挡住就会引发全表请求排队。
注意:把 INT 改成 BIGINT 只是扩大列,不会改变已有值;如果表里已经有被钳到上限的脏数据,需要先修数据再改类型。
五、类型改不动时的兜底
- 归档冷数据:把历史分区
EXCHANGE PARTITION到归档表,降低自增水位压力(本质是腾出号段,不解决上限)。 - 分库分表:按用户维度拆分后,每个分片独立自增,号段消耗速度除以分片数。
- 改造主键生成:换成雪花 ID 或号段服务(Leaf/Seata),主键从自增改为应用生成,需要处理索引顺序与页分裂。
- 业务降级:写入落到新表,老表只读,读路径做双读兼容。
常见追问
- 自增主键为什么会跳号? 事务回滚不回收已分配的值;批量插入时
innodb_autoinc_lock_mode会预分配号段;INSERT ... ON DUPLICATE KEY UPDATE也会消耗一个号。 - 三种自增锁模式的区别? 0 传统表锁、1 连续(8.0 之前默认,批量插入会阻塞)、2 交错(8.0 默认,并发最好但号段有空洞)。
AUTO_INCREMENT能手动改小吗? 可以ALTER TABLE ... AUTO_INCREMENT = 1,但不会低于当前最大值,且对已有数据无影响。- UUID 主键能避免耗尽吗? 能,但随机分布会让聚簇索引页分裂严重、二级索引膨胀,写入吞吐通常下降数倍。
- 自增主键用完了能重置吗? 只有在数据已清理、最大值下降后才可能,属于典型的高风险操作,生产环境基本不作考虑。
小结
自增耗尽不是「溢出报错」而是「持续主键冲突」。工程上的正解是三点:新表一律 BIGINT UNSIGNED;用 auto_increment 占比做容量水位监控;真到要改类型时走在线 DDL 或影子表工具,并避开长事务。