自增主键用完了怎么办?

面试考察点

  1. 数据类型理解:面试官不仅仅是想知道 “用完了怎么办”,更是想考察你是否清楚不同整数类型的取值范围,以及 INT 和 BIGINT 的区别。
  2. 系统设计思维:考察你是否具备前瞻性的系统设计能力,能否在表设计阶段就考虑到数据量增长的问题。
  3. 分布式 ID 方案:当单机自增主键不够用时,是否了解分布式 ID 生成方案(雪花算法、UUID 等),这是高并发系统的必备知识。

核心答案

自增主键用完的后果:当自增 ID 达到数据类型的最大值后,再次插入数据会报错 Duplicate entry 'xxx' for key 'PRIMARY',新的数据无法写入。 各整数类型的主键容量:

数据类型 字节数 最大值 容量级
TINYINT 1 127(有符号)/ 255(无符号) 百级
SMALLINT 2 32767 / 65535 万级
MEDIUMINT 3 8388607 / 16777215 百万级
INT 4 2147483647 / 4294967295 21 亿 / 42 亿
BIGINT 8 9223372036854775807 / … 922 亿亿

一句话总结:主键用完会导致插入失败,解决方案有 改用更大的类型(BIGINT) 或 使用分布式 ID(雪花算法)。

深度解析

一、自增主键用完会发生什么?

当自增 ID 达到上限后,MySQL 的行为如下:

项目 数值
BIGINT UNSIGNED 最大值 18,446,744,073,709,551,615
假设每秒插入 1,000,000 条
每天插入 86,400,000,000 条(864 亿)
每年插入 31,536,000,000,000 条(31 万亿)
用完 BIGINT 需要 约 584,942 年

结论:BIGINT 在人类可预见的范围内是用不完的。

上图展示了自增主键用完后的情况。关键点说明:

  • 自增到最大值:当 AUTO_INCREMENT 达到数据类型的上限时,无法继续递增
  • 插入失败:MySQL 尝试使用最大值插入,但该值已存在,导致主键冲突
  • 删除不回退:即使删除了中间的记录,AUTO_INCREMENT 值也不会变小
  • 影响范围:所有依赖自增主键的插入操作都会失败

二、解决方案一:改用更大的数据类型

最简单直接的方案:将 INT 改为 BIGINT。

-- 修改前:INT 最大约 21 亿(有符号)或 42 亿(无符号)
CREATE TABLE orders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32)
);

-- 修改后:BIGINT 最大约 922 亿亿
ALTER TABLE orders MODIFY id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY;

BIGINT 有多大?

注意事项:

  • 修改字段类型会锁表,大表操作需要谨慎(使用 pt-online-schema-change 等工具)
  • 建议新表设计时就直接使用 BIGINT,省去后续迁移的麻烦

三、解决方案二:使用分布式 ID —— 雪花算法(Snowflake)

对于分布式系统,单机自增主键不够用或存在冲突风险,推荐使用 雪花算法 生成分布式 ID。

组成部分 位数 说明 取值范围
符号位 1 始终为 0,保证 ID 为正数 0
时间戳 41 毫秒级精度,可用约 69 年 0 ~ 2^41 - 1
机器 ID 10 支持 1024 台机器节点,分布式环境唯一标识 0 ~ 1023
序列号 12 同一毫秒内序列,可生成 4096 个 ID 0 ~ 4095

单机每毫秒最多生成 4096 个 ID,即每秒约 400 万个 ID。特性:趋势递增、全局唯一、高性能、分布式友好。

上图展示了雪花算法的 ID 结构。核心优势在于:

  • 全局唯一:通过机器 ID 区分不同节点,保证分布式环境下的唯一性
  • 趋势递增:基于时间戳生成,ID 大致按时间递增,利于索引
  • 高性能:本地生成,不依赖数据库,单机每秒可生成 400 万个 ID
  • 无需协调:各节点独立生成,不需要分布式锁或中心化服务
    Java 实现示例:
public class SnowflakeIdGenerator {
// 起始时间戳(2024-01-01)
private final long twepoch = 1704038400000L;

// 机器 ID 所占位数
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;

// 序列号所占位数
private final long sequenceBits = 12L;

// 机器 ID 最大值:31
private final long maxWorkerId = ~(-1L << workerIdBits);

// 序列号最大值:4095
private final long sequenceMask = ~(-1L << sequenceBits);

// 机器 ID 左移位数
private final long workerIdShift = sequenceBits;

// 时间戳左移位数
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;

public SnowflakeIdGenerator(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id error");
}
this.workerId = workerId;
}

public synchronized long nextId() {
long timestamp = System.currentTimeMillis();

// 时钟回拨检测
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards!");
}

// 同一毫秒内,序列号递增
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 序列号溢出,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}

lastTimestamp = timestamp;

// 组装 ID
return ((timestamp - twepoch) << timestampLeftShift)
| (workerId << workerIdShift)
| sequence;
}

private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}

四、各种 ID 生成方案对比

方案 全局唯一 趋势递增 性能 推荐度
MySQL 自增 否 是 ★★ 单机可用
UUID 是 否 ★★★★★ 不推荐
雪花算法 是 是 ★★★★★ 推荐
Redis 自增 是 是 ★★★★ 推荐
号段模式 是 是 ★★★★★ 推荐

UUID 的问题:36 字符字符串,占用空间大;完全无序,导致 B+ 树频繁页分裂,影响插入性能;不利于聚簇索引的顺序存储。

为什么 UUID 不推荐作为主键?

  • 页分裂问题:UUID 是无序的,插入时会导致 B+ 树频繁分裂
  • 存储空间大:36 字符 vs 8 字节的 BIGINT
  • 索引效率低:无序 ID 导致缓存命中率下降

五、最佳实践建议

1. 新表设计直接用 BIGINT

-- 推荐写法
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-- 其他字段...
) ENGINE=InnoDB;

2. 分布式系统用雪花算法

// 使用成熟的开源实现
// 如:Mybatis-Plus 的 IdWorker、百度 UidGenerator、美团 Leaf
@IdType(type = IdType.ASSIGN_ID) // Mybatis-Plus 雪花算法
private Long id;

3. 设置合理的自增起始值和步长

-- 多主架构下,避免 ID 冲突
-- 服务器 1:起始值 1,步长 2(生成 1, 3, 5, ...)
SET GLOBAL auto_increment_offset = 1;
SET GLOBAL auto_increment_increment = 2;

-- 服务器 2:起始值 2,步长 2(生成 2, 4, 6, ...)
SET GLOBAL auto_increment_offset = 2;
SET GLOBAL auto_increment_increment = 2;

面试高频追问

  1. 追问一:为什么 UUID 不适合做主键?
    • 答:UUID 无序会导致 B+ 树频繁页分裂,影响插入性能;同时占用空间大(36 字符 vs 8 字节),降低索引效率。
  2. 追问二:雪花算法有什么缺点?
    • 答:依赖服务器时钟,时钟回拨会导致 ID 重复或异常;需要处理机器 ID 的分配问题(可通过 ZooKeeper/Redis 统一管理)。
  3. 追问三:INT 和 BIGINT 对性能有影响吗?
    • 答:影响很小。主键索引的查找时间复杂度是 O(log n),与主键大小关系不大;但 BIGINT 占用更多存储空间,可能影响内存中的缓存效率。

常见面试变体

  • “MySQL 主键用完了会怎么样?”
  • “为什么推荐用 BIGINT 做主键?”
  • “分布式 ID 生成方案有哪些?”
  • “雪花算法的原理是什么?”

记忆口诀

主键 ID 方案选择:

  1. 单机用 BIGINT:容量够大,简单可靠
  2. 分布式用雪花:全局唯一,趋势递增
  3. 远离 UUID:无序导致页分裂

总结

自增主键用完会导致插入失败,解决方案有两种:单机场景改用 BIGINT(约 922 亿亿容量,足够使用);分布式场景使用雪花算法生成全局唯一、趋势递增的 ID。新表设计建议直接用 BIGINT UNSIGNED,避免后续迁移成本。


声明:本文转载自小哈的博客 《自增主键用完了怎么办?》,版权归原文作者所有。转载请保留本声明与原文链接。