千家信息网

为什么MySQL自增主键不单调也不连续

发表于:2025-01-21 作者:千家信息网编辑
千家信息网最后更新 2025年01月21日,这篇文章主要讲解了"为什么MySQL自增主键不单调也不连续",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"为什么MySQL自增主键不单调也不连续"吧!当
千家信息网最后更新 2025年01月21日为什么MySQL自增主键不单调也不连续

这篇文章主要讲解了"为什么MySQL自增主键不单调也不连续",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"为什么MySQL自增主键不单调也不连续"吧!

当我们在使用关系型数据库时,主键(Primary Key)是无法避开的概念,主键的作用就是充当记录的标识符,我们能够通过标识符在一张表中定位到唯一的记录。

在关系型数据库中,我们会选择记录中多个字段的最小子集作为该记录在表中的唯一标识符[^1],根据关系型数据库对主键的定义,我们既可以选择单个列作为主键,也可以选择多个列作为主键,但是主键在整个记录中必须存在并且唯一。最常见的方式当然是使用 MySQL 默认的自增 ID 作为主键,虽然使用其他策略设置的主键也是合法的,但是不是通用的以及推荐的做法。

图 1 - MySQL 的主键

MySQL 中默认的 AUTO_INCREMENT 属性在多数情况下可以保证主键的连续性,我们通过 show create table 命令可以在表的定义中能够看到 AUTO_INCREMENT属性的当前值,当我们向当前表中插入数据时,它会使用该属性的值作为插入记录的主键,而每次获取该值也都会将它加一。

CREATE TABLE `trades` (   `id` bigint(20) NOT NULL AUTO_INCREMENT,   ...   `created_at` timestamp NULL DEFAULT NULL,   PRIMARY KEY (`id`), ) ENGINE=InnoDB AUTO_INCREMENT=17130 DEFAULT CHARSET=utf8mb4

在很多开发者的认知中,MySQL 的主键都应该是单调递增的,但是在我们与 MySQL 打交道的过程中会遇到两个问题,首先是记录的主键并不连续,其次是可能会创建多个主键相同的记录,我们将从以下的两个角度回答 MySQL 不单调和不连续的原因:

  • 较早版本的 MySQL 将 AUTO_INCREMENT 存储在内存中,实例重启后会根据表中的数据重新设置该值;

  • 获取 AUTO_INCREMENT 时不会使用事务锁,并发的插入事务可能出现部分字段冲突导致插入失败;

需要注意的是,我们在这篇文章中讨论的是 MySQL 中最常见的 InnoDB 存储引擎,MyISAM 等其他引擎提供的 AUTO_INCREMENT 实现原理不在本文的讨论范围中。

删除记录

AUTO_INCREMENT 属性虽然在 MySQL 中十分常见,但是在较早的 MySQL 版本中,它的实现还比较简陋,InnoDB 引擎会在内存中存储一个整数表示下一个被分配到的 ID,当客户端向表中插入数据时会获取 AUTO_INCREMENT 值并将其加一。

图 2 - AUTO_INCREMENT 的使用

因为该值存储在内存中,所以在每次 MySQL 实例重新启动后,当客户端第一次向 table_name 表中插入记录时,MySQL 会使用如下所示的 SQL 语句查找当前表中 id 的最大值,将其加一后作为待插入记录的主键,并作为当前表中 AUTO_INCREMENT 计数器的初始值[^2]。

SELECT MAX(ai_col) FROM table_name FOR UPDATE;

如果让作者实现 AUTO_INCREMENT,在最开始也会使用这种方法。不过这种实现虽然非常简单,但是如果使用者不严格遵循关系型数据库的设计规范,就会出现如下所示的数据不一致的问题:

图 3 - 5.7 版本之前的 AUTO_INCMRENT

因为重启了 MySQL 的实例,所以内存中的 AUTO_INCREMENT 计数器会被重置成表中的最大值,当我们再向表中插入新的 trades 记录时会重新使用 10 作为主键,主键也就不是单调的了。在新的 trades 记录插入之后,executions 表中的记录就错误的引用了新的 trades,这其实是一个比较严重的错误。

然而这也不完全是 MySQL 的问题,如果我们严格遵循关系型数据库的设计规范,使用外键处理不同表之间的联系,就可以避免上述问题,因为当前 trades 记录仍然有外部的引用,所以外键会禁止 trades 记录的删除,不过多数公司内部的 DBA 都不推荐或者禁止使用外键,所以确实存在出现这种问题的可能。

然而在 MySQL 8.0 中,AUTO_INCREMENT 计数器的初始化行为发生了改变,每次计数器的变化都会写入到系统的重做日志(Redo log)并在每个检查点存储在引擎私有的系统表中[^3]。

In MySQL 8.0, this behavior is changed. The current maximum auto-increment counter value is written to the redo log each time it changes and is saved to an engine-private system table on each checkpoint. These changes make the current maximum auto-increment counter value persistent across server restarts.

当 MySQL 服务被重启或者处于崩溃恢复时,它可以从持久化的检查点和重做日志中恢复出最新的 AUTO_INCREMENT 计数器,避免出现不单调的主键也解决了这里提到的问题。

并发事务

为了提高事务的吞吐量,MySQL 可以处理并发执行的多个事务,但是如果并发执行多个插入新记录的 SQL 语句,可能会导致主键的不连续。如下图所示,事务 1 向数据库中插入 id = 10 的记录,事务 2 向数据库中插入 id = 11 和 id = 12 的两条记录:

图 4 - 并发事务的执行

不过如果在最后事务 1 由于插入的记录发生了唯一键冲突导致了回滚,而事务 2 没有发生错误而正常提交,在这时我们会发现当前表中的主键出现了不连续的现象,后续新插入的数据也不再会使用 10 作为记录的主键。

图 5 - 不连续的主键

这个现象背后的原因也很简单,虽然在获取 AUTO_INCREMENT 时会加锁,但是该锁是语句锁,它的目的是保证 AUTO_INCREMENT 的获取不会导致线程竞争,而不是保证 MySQL 中主键的连续[^4]。

上述行为是由 InnoDB 存储引擎提供的 innodb_autoinc_lock_mode 配置控制的,该配置决定了获取 AUTO_INCREMENT 计时器时需要先得到的锁,该配置存在三种不同的模式,分别是传统模式(Traditional)、连续模式(Consecutive)和交叉模式(Interleaved)[^5],其中 MySQL 使用连续模式作为默认的锁模式:

(1) 传统模式 innodb_autoinc_lock_mode = 0;

在包含 AUTO_INCREMENT 属性的表中插入数据时,所有的 INSERT 语句都会获取表级别的 AUTO_INCREMENT 锁,该锁会在当前语句执行后释放;

(2) 连续模式 innodb_autoinc_lock_mode = 1;

  • INSERT ... SELECT、REPLACE ... SELECT 以及 LOAD DATA 等批量的插入操作需要获取表级别的 AUTO_INCREMENT 锁,该锁会在当前语句执行后释放;

  • 简单的插入语句(预先知道插入多少条记录的语句)只需要获取获取 AUTO_INCREMENT 计数器的互斥锁并在获取主键后直接释放,不需要等待当前语句执行完成;

(3) 交叉模式 innodb_autoinc_lock_mode = 2;

所有的插入语句都不需要获取表级别的 AUTO_INCREMENT 锁,但是当多个语句插入的数据行数不确定时,可能存在分配相同主键的风险;

这三种模式都不能解决 MySQL 自增主键不连续的问题,想要解决这个问题的终极方案是串行执行所有包含插入操作的事务,也就是使用数据库的最高隔离级别 —— 可串行化(Serialiable)。当然直接修改数据库的隔离级别相对来说有些简单粗暴,基于 MySQL 或者其他存储系统实现完全串行的插入也可以保证主键在插入时的连续,但是仍然不能避免删除数据导致的不连续。

总结

早期 MySQL 的主键既不是单调的,也不是连续的,这些都是在当时工程上做出的一些选择,如果严格地按照关系型数据库的设计规范,MySQL 最初的设计造成问题的概率也比较低,只有当被删除的主键被外部系统引用时才会影响数据的一致性,但是今天使用方式的不同却增加出错的可能性,而 MySQL 也在 8.0 中持久化了 AUTO_INCREMENT 以避免该问题的出现。

MySQL 中不连续的主键又是一个工程设计向性能低头的例子,牺牲主键的连续性来支持数据的并发插入,最终提高了 MySQL 服务的吞吐量,作者在几年前刚刚使用 MySQL 时就遇到过这个问题,但是当时并没有深究背后的原因,今天重新理解该问题背后的设计决策也是个非常有趣的过程。我们在这里简单总结一下本文的内容,重新回到今天的问题 — 为什么 MySQL 的自增主键不单调也不连续:

  • MySQL 5.7 版本之前在内存中存储 AUTO_INCREMENT计数器,实例重启后会根据表中的数据重新设置,在删除记录后重启就可能出现重复的主键,该问题在 8.0 版本使用重做日志解决,保证了主键的单调性;

  • MySQL 插入数据获取 AUTO_INCREMENT 时不会使用事务锁,而是会使用互斥锁,并发的插入事务可能出现部分字段冲突导致插入失败,想要保证主键的连续需要串行地执行插入语句;

感谢各位的阅读,以上就是"为什么MySQL自增主键不单调也不连续"的内容了,经过本文的学习后,相信大家对为什么MySQL自增主键不单调也不连续这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

数据 问题 事务 语句 数据库 模式 存储 不单 计数器 多个 保证 设计 内存 属性 引擎 版本 级别 单调 实例 系统 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 安卓网页游戏服务器 四川时代网络技术分类技术 作为网络安全的集中监视点 校园网免服务器 数据库如何做网页视频 谷歌服务器的云盘怎么用 文本解析提取到数据库 上海吾牛网络技术有限公司 为什么没有服务器的身份验证 数据库收缩以后导致数据库打不开 宝山区什么是网络技术分类 软件开发如何计算利率 启迪控股 国家网络安全 湖北电脑软件开发收费报价表 论文数据库检索使用方法 潮型库互联网科技有限公司怎么样 数据库及软件技术 帝国神话私人服务器怎么删除重建 郑州网络安全大会 榆树有名的网络技术服务排名靠前 利亚方舟影楼管理系统服务器配置 代理服务器管理员权限 加强网络安全的英文 福建信息化软件开发价格大全 初中网络安全资料 网络安全法律法规形成过程 启迪控股 国家网络安全 网络安全大讲堂青少年专场 专业技术网络安全继续教育 安卓应用软件开发哪家有名
0