MySQL 自增主键用完了怎么办?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 数据类型理解:面试官不仅仅是想知道 "用完了怎么办",更是想考察你是否清楚不同整数类型的取值范围,以及 INTBIGINT 的区别。

  2. 系统设计思维:考察你是否具备前瞻性的系统设计能力,能否在表设计阶段就考虑到数据量增长的问题。

  3. 分布式 ID 方案:当单机自增主键不够用时,是否了解分布式 ID 生成方案(雪花算法、UUID 等),这是高并发系统的必备知识。

核心答案

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

各整数类型的主键容量

数据类型字节数最大值容量级
TINYINT1127(有符号)/ 255(无符号)百级
SMALLINT232767 / 65535万级
MEDIUMINT38388607 / 16777215百万级
INT42147483647 / 429496729521 亿 / 42 亿
BIGINT89223372036854775807 / ...922 亿亿

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

深度解析

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

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

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

  • 自增到最大值:当 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。

上图展示了雪花算法的 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 生成方案对比

为什么 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,避免后续迁移成本。