社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

面试官:线上MySQL的自增id用尽怎么办?

鸭哥聊Java • 2 周前 • 45 次点击  

作为 Java 开发,我们在日常项目里和 MySQL 打交道是非常频繁的,尤其是各种基于自增主键的设计,不管是电商订单号、用户ID还是某个配置表的记录索引,几乎都绕不开这个逻辑。而今天我们要聊的,就是一个看上去很“遥远”、但却实实在在存在的问题:MySQL 的自增 ID 用完了怎么办?

这不是个段子,也不是危言耸听。

自增 ID 真有上限吗?

很多刚入行的小伙伴听到这个问题,第一反应可能是:“自然数不是无限的吗?哪来的用完一说?”

别忘了,这可是在计算机里,MySQL 又不是玄学数据库,它定义的是一个 定长整数类型。比如 int unsigned 是 4 字节,那上限就是 2^32 - 1,也就是 4294967295。再插,就报错。

举个栗子:

CREATE TABLE t (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT = 4294967295;

你插一条数据:

INSERT INTO t VALUES(NULL); -- 成功,id=4294967295

再插:

INSERT INTO t VALUES(NULL); -- 报错:Duplicate entry '4294967295' for key 'PRIMARY'

你没看错,它还真不是给你从头开始,而是原地打转。下一条 insert 仍然试图拿 4294967295,结果直接和上一次冲突了,主键重复。

作为 Java 开发,这种问题你遇到一次,代价可能就是线上故障、业务挂掉、甚至数据丢失。

所以建表时一定不能“图省事”用 int 或 int unsigned能用 BIGINT UNSIGNED 就别省字节了。

那我建了 BIGINT 就万无一失了吗?

大概率是了,BIGINT UNSIGNED 是 8 字节,上限 2^64 - 1,这辈子别说你,可能你孙子都插不完。

但你得注意,MySQL 不光一个地方在用“自增”,除了我们建表时自定义的自增 ID,还有 InnoDB 内部自动生成的 row_id、Xid、事务ID 等等,它们也有上限,也会“归零”。

我们一个一个讲。

如果你没定义主键,那 InnoDB 会替你造一个

是的,InnoDB 会给每行数据生成一个内部的 row_id。但这个 row_id 并不是用 BIGINT 写入数据页的,而是只用了 6 字节

也就是说,虽然 row_id 的变量是 BIGINT 类型,但它真正落盘写入表里的 ID,只能表示从 0 到 2^48 - 1

那如果某一天用完了呢?

答: 它会归零,然后从 0 开始再循环一次。

我当时看到这里是真的头皮一紧。归零这词在技术里,一般就不是个好兆头……

更可怕的是,InnoDB 不是检查这个 row_id 有没有重复,而是直接拿来用。所以新的数据会把之前那个 row_id 相同的老数据覆盖掉数据直接丢了

row_id 用完 = 数据丢失

怎么验证这个问题?用 gdb 去改内存里 InnoDB 的 row_id 值,比如直接把它改成 2^48,再往表里插一条数据,row_id 会变成 0。再插一条,row_id 变成 1。如果你的表里原来就有 row_id=1 的老数据,那对不起,它就这样被覆盖了……

这也是为啥我们一直强调:不要让 InnoDB 替你生成主键,你得自己建一个明确的 BIGINT UNSIGNED AUTO_INCREMENT 主键,这样才能在 ID 用完时明确抛错(主键冲突),而不是“神不知鬼不觉”地覆盖原有数据。

在数据安全性这事上,宁可不可用,也不能不可靠。插入失败还能重试,数据丢了就是永久损失。

MySQL 的事务 ID(trx_id)也不是无限的

InnoDB 的每条数据都会记录“我是谁写的”——也就是它被哪个事务更新过,这个事务编号就是 trx_id。这个 trx_id 用来做可见性判断,决定你这个事务看到的是哪一版的数据。

那这个 trx_id 是怎么生成的?

也是个全局变量:max_trx_id。每次事务更新数据,它就 +1,发一个新的给你。

但你注意,它的存储空间也是 6 字节

又是熟悉的 2^48,也就是说 trx_id 能表示的范围也是 0 到 2^48 - 1

问题来了,如果 trx_id 也到达上限了,怎么办?

答:继续从 0 开始。

听着耳熟吧?和 row_id 一模一样的逻辑。

脏读 bug:trx_id 重置后的连锁反应

你要是事务 ID 重置了,也许你看一眼觉得没啥,但实际上可能会引发严重的脏读。

简单解释一下:

每个事务启动的时候,InnoDB 会记录一个“低水位线”,就是它当前能看到最旧的数据版本(也就是那一刻活跃事务的最小 trx_id)。如果你启动事务时,max_trx_id 正好是 2^48 - 1,你拿到的事务 ID 就是这个极限值。

然后你有个并发事务继续干活,新插入的数据版本,它的 ID 是 0(因为归零了)。你这个老事务去读的时候一看,诶,0 2^48 - 1,那是“之前的数据”,于是就给你读出来了。

可问题是,这数据明明是刚插的。你没 commit,你照样能读。这就是脏读。

而且只要你一旦回到了 0,这事就天天都在发生。你 MySQL 重启都没用,因为 max_trx_id 是持久化保存的,重启不会清零。

虽然这个 bug 是理论上的,得 TPS 到百万级、连续运行 17 年左右才能触发……但只要时间够长,它就是必然会发生的事。是不是突然觉得冷汗直冒?

事务 Xid 也是递增的,它的 ID 会不会重复?

MySQL 的 Server 层会给每个事务分配一个 Xid(事务 ID),用于 binlog 和 redo log 的标识。

这个 Xid 是从 global_query_id 来的,而 global_query_id 是个内存变量,重启就清零。

那是不是重启后就可能有重复的 Xid?是的,但影响不大。

因为每次重启 MySQL,都会生成新的 binlog 文件,而只要同一个 binlog 文件里没有重复 Xid,就不会有问题。

而且 global_query_id 是 8 字节的,想让它自增满一次达到 2^64 的上限,要执行 1.84 * 10^19 条语句……理论上确实可能,但现实中几乎不可能。

所以这块你可以放心。

线程 ID(thread_id)也不是无限的

每个 MySQL 连接都分配一个线程 ID,也是自增的。这玩意是 int 类型,4 字节,最多 2^32 - 1,也会归零。

但你 show processlist 的时候不会看到重复的线程 ID,因为系统里用了一张唯一表来记录线程,如果之前那个线程还没退出,新线程是拿不到重复 ID 的,MySQL 会继续找空位。

所以这块比较安全。

所以,作为开发我该注意什么?

  1. 所有自增 ID,建表的时候都用 BIGINT UNSIGNED,不要省事用 INT

  2. 一定要明确声明主键,别让 InnoDB 自动帮你生成 row_id

  3. 如果你做的是高并发、写入密集的系统(比如交易系统、日志系统),还得考虑将来是不是可能触发这些上限。

  4. 某些只读事务不会申请 trx_id(比如普通 SELECT),但是带 FOR UPDATE 的查询是会申请的,要注意性能和锁冲突。

  5. 永远不要把“我们现在数据量不大”当作建表偷懒的理由。数据库是活的,用几年之后它会长成你无法控制的样子。

最后

MySQL 是靠自增 ID 在维持数据秩序,但你不能指望它“自个长命百岁不出错”。任何设计都必须考虑“最坏情况”,特别是这些“归零”带来的潜在 bug,虽然离我们现在还有点距离,但你一旦踩到,就是线上直接拉闸的节奏。

写代码的时候,别总想着“先用着再说”,这世上没有免费的懒惰,后面你会用加倍的代价来还。

最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek


同时,也可以看我写的这篇文章《DeepSeek满血复活,直接起飞!》来进行本地搭建。

对编程、职场感兴趣的同学,可以链接我,微信:yagebug  拉你进入“程序员交流群”。
🔥鸭哥私藏精品 热门推荐🔥

鸭哥作为一名老码农,整理了全网最全 《Java高级架构师资料合集》
资料包含了《IDEA视频教程》《最全Java面试题库》、最全项目实战源码及视频》及《毕业设计系统源码》总量高达 650GB 。全部免费领取!全面满足各个阶段程序员的学习需求。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/182341
 
45 次点击