首页
学习
活动
专区
圈层
工具
发布

MySQL 中 AUTO_INCREMENT 列达到最大值时会发生什么?

订单表突然不写了,Java 这边只看到一行异常:

Duplicate entry '2147483647' for key 'order_info.PRIMARY'

这类报错我第一眼不会先去怀疑并发,也不会先翻业务代码。先看表结构。

SHOW CREATE TABLE order_info\G

CREATE TABLE `order_info` (

`id` int NOT NULL AUTO_INCREMENT,

`user_id` bigint NOT NULL,

`order_no` varchar(64) NOT NULL,

`created_at` datetime NOT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2147483647;

看到AUTO_INCREMENT=2147483647,基本就不用猜了。

这个表的主键是int,而且不是unsigned。MySQL 里INT的有符号范围到2147483647,INT UNSIGNED才能到4294967295;BIGINT的范围更大。这个范围不是展示长度决定的,int(11)那个 11 也救不了命,它不是“能存 11 位数字”。MySQL 官方手册里也写得很直,INT和BIGINT的边界就是数据类型本身决定的。([MySQL 开发者区][1])

AUTO_INCREMENT 正常生成值时,一般就是当前列最大值再加 1。插入时给NULL或者不写这个字段,MySQL 会自动拿下一个序列值。([MySQL 开发者区][2])

问题来了:当前已经是2147483647,下一个还能给谁?

这时候继续插入:

INSERT INTO order_info(user_id, order_no, created_at)

VALUES (10086, 'O202605200001', NOW());

线上常见结果就是:

ERROR 1062 (23000): Duplicate entry '2147483647' for key 'order_info.PRIMARY'

也可能碰到:

ERROR 1467 (HY000): Failed to read auto-increment value from storage engine

别被Duplicate entry带偏了。它不是你代码里手动传了重复 id,也不是雪花算法重复,也不是 MQ 重放。它就是自增列顶到头了,MySQL 已经没法给出一个合法的新值。

Java 侧一般长这样:

public Long createOrder(long userId, String orderNo) {

  String sql = """

      insert into order_info(user_id, order_no, created_at)

      values (?, ?, now())

      """;

  try {

      KeyHolder keyHolder = new GeneratedKeyHolder();

      jdbcTemplate.update(conn -> {

          PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

          ps.setLong(1, userId);

          ps.setString(2, orderNo);

          return ps;

      }, keyHolder);

      return Objects.requireNonNull(keyHolder.getKey()).longValue();

  } catch (DuplicateKeyException e) {

      if (e.getMessage() != null && e.getMessage().contains("2147483647")) {

          throw new IllegalStateException("order_info.id 自增值已经打满,别重试,先扩字段", e);

      }

      throw e;

  }

}

这里我特意写了“别重试”。

很多系统一看到插入失败,就进重试队列。这个场景重试没有意义。你重试 10 次,就是打 10 次数据库,顺便把告警刷爆。自增值满了,不会因为你睡 3 秒再插一次就自己恢复。

真正要查的是这几个值:

SELECT

  t.table_schema,

  t.table_name,

  t.auto_increment,

  c.column_type

FROM information_schema.tables t

JOIN information_schema.columns c

ON c.table_schema = t.table_schema

AND c.table_name = t.table_name

WHERE t.table_schema = 'trade'

AND t.table_name = 'order_info'

AND c.extra LIKE '%auto_increment%';

如果结果是这样:

table_name   auto_increment   column_type

order_info   2147483647       int

那就别在应用层绕了,改表。

小表可以直接:

ALTER TABLE order_info

MODIFY id BIGINT NOT NULL AUTO_INCREMENT;

但线上大表别这么莽。ALTER TABLE改主键类型,可能会重建表,锁表时间、磁盘空间、复制延迟都要看。尤其主键被一堆子表引用,或者二级索引很多,这个操作不是“改个字段”这么轻。

而且有个坑要记住:MySQL 用MODIFY改字段时,原来的属性不会自动完整保留。比如你原来是INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',你只写BIGINT,那UNSIGNED、默认值、注释这类属性可能就被你顺手丢了。官方文档也专门提醒过,改字段定义时要把需要保留的属性重新写全。([MySQL 开发者区][3])

所以我一般会先拿原结构,再手写完整 DDL:

ALTER TABLE order_info

MODIFY id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单主键';

应用层也要一起看。Java 里如果还用Integer接主键,那数据库改成BIGINT只是改了一半。

这种代码也得改:

public class OrderRecord {

  private Long id;

  private Long userId;

  private String orderNo;

  public Long id() {

      return id;

  }

}

别数据库是BIGINT,Java DTO 还是Integer,接口 JSON 里再被前端当 number 随便处理。后面会变成另一类事故。

临时止血怎么办?

如果表里有很多历史垃圾数据,理论上可以归档删除,然后调整AUTO_INCREMENT。但这只在“最大 id 没有真的打满”的情况下有用。

SELECT MAX(id) FROM order_info;

ALTER TABLE order_info AUTO_INCREMENT = 1800000000;

如果MAX(id)已经是2147483647,删中间数据没用。AUTO_INCREMENT 看的是还能不能生成下一个合法值,不是看表里空了多少洞。id 用掉就是用掉了,中间删掉的 100 万个 id,不会自动拿回来继续发。

所以这个问题最好的处理时间,不是报错那天,而是还剩一截的时候。

我会加一个很土但很管用的巡检:

public void checkAutoIncrementHeadroom() {

  String sql = """

      select t.table_schema, t.table_name, t.auto_increment, c.column_type

      from information_schema.tables t

      join information_schema.columns c

        on c.table_schema = t.table_schema

       and c.table_name = t.table_name

      where c.extra like '%auto_increment%'

        and t.table_schema not in ('mysql','sys','performance_schema','information_schema')

      """;

  jdbcTemplate.query(sql, rs -> {

      String type = rs.getString("column_type");

      long next = rs.getLong("auto_increment");

      long max = switch (type) {

          case "int" -> 2_147_483_647L;

          case "int unsigned" -> 4_294_967_295L;

          case "bigint" -> Long.MAX_VALUE;

          default -> -1L;

      };

      if (max > 0 && next > max * 0.85) {

          log.warn("auto_increment 快满了: {}.{}, next={}, type={}, used={}%",

                  rs.getString("table_schema"),

                  rs.getString("table_name"),

                  next,

                  type,

                  next * 100 / max);

      }

  });

}

这个检查不用多高级,每天跑一次就行。真等到插入失败,业务已经在门口排队了。

还有一点,别上来就把所有表都改成BIGINT UNSIGNED。小字典表、小配置表,用INT没问题。订单、流水、日志、消息、轨迹、行为记录这类持续增长表,建表第一天就别抠那几个字节。

主键类型选小了,前几年看不出来。

等它出事的时候,通常不是一张表出事,是写入链路、重试队列、复制延迟、报警一起跟着响。这个事故没什么玄学,就是建表时少想了一步。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Op7MAj1syOG-WW_xOxyfDX_A0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券