自增主键用完了怎么办?

#中间件#面试八股

自增主键用完了怎么办?

结论:自增主键耗尽的表现是「插入报主键冲突」,而不是报溢出;此时表已经写不进去,唯一稳妥的出路是提前把类型换成 BIGINT UNSIGNED。所以真正的问题不是「用完了怎么办」,而是「怎么在用到一半时就发现」。

一、各整数类型的上限

类型有符号上限无符号上限按每秒 1000 条估算可用时长
TINYINT127255不到 1 秒
SMALLINT3276765535约 1 分钟
MEDIUMINT838860716777215约 4.6 小时
INT21474836474294967295约 49.7 天
BIGINT9.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 或影子表工具,并避开长事务。

笔记加载中…