为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题,可以在文章下面留言。

当我们在使用关系型数据库时,主键(Primary Key)是无法避开的概念,主键的作用就是充当记录的标识符,我们能够通过标识符在一张表中定位到唯一的记录,作者在 为什么总是需要无意义的 ID 曾经介绍过为什么不应该使用有意义的字段来充当唯一标识符,感兴趣的读者可以了解一下。

在关系型数据库中,我们会选择记录中多个字段的最小子集作为该记录在表中的唯一标识符[^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 = 11id = 12 的两条记录:

图 4 - 并发事务的执行

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

图 5 - 不连续的主键

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

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

  • 传统模式 innodb_autoinc_lock_mode = 0

    • 在包含 AUTO_INCREMENT 属性的表中插入数据时,所有INSERT 语句都会获取表级别AUTO_INCREMENT 锁,该锁会在当前语句执行后释放;
  • 连续模式 innodb_autoinc_lock_mode = 1
    • INSERT ... SELECTREPLACE ... SELECT  以及 LOAD DATA 等批量的插入操作需要获取表级别AUTO_INCREMENT 锁,该锁会在当前语句执行后释放;
    • 简单的插入语句(预先知道插入多少条记录的语句)只需要获取获取 AUTO_INCREMENT 计数器的互斥锁并在获取主键后直接释放,不需要等待当前语句执行完成;
  • 交叉模式 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 时不会使用事务锁,而是会使用互斥锁,并发的插入事务可能出现部分字段冲突导致插入失败,想要保证主键的连续需要串行地执行插入语句;

到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:

  • MyISAM 和其他的存储引擎如何存储 AUTO_INCREMENT 计数器?
  • MySQL 中的 auto_increment_incrementauto_increment_offset 是用来做什么的?

求关注

求转发

mysql自增_面试官:为什么 MySQL 的自增主键不单调也不连续?相关推荐

  1. 无法去掉自增标识_为什么 MySQL 的自增主键不单调也不连续

    为什么这么设计(Why's THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点.对具体实现造成的影响 ...

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

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 转自:真没什么逻辑/Draveness 当我们在使用关系型数据库时 ...

  3. java面试mysql的引擎_面试官:你用过mysql哪些存储引擎,请分别展开介绍一下

    这是高级开发者面试时经常被问的问题.实际我们在平时的开发中,经常会遇到的,在用SQLyog等工具创建表时,就有一个引擎项要你去选.如下图: Mysql的存储引擎有这么多种,实际我们在平时用的最多的莫过 ...

  4. mysql 唯一索引_面试官:谈谈你对mysql索引的认识?

    引言 大家好,我渣渣烟.我曾经写过一篇<面试官:讲讲mysql表设计要注意啥>,当时写完后,似乎效果还行! 于是呢,决定再来一个mysql的数据库专题,这篇我们就来谈谈关于索引方面的mys ...

  5. mysql怎么用_面试官都是这样发问的,连环冲锋炮,看你怎么抵挡(上)

    本内容来源于和尚 16 年毕业的学长,先在 58,后阿里,如今准备跳槽了,以下内容为他的最近面试经历 我最近从大厂离职之后在合肥呆了个把月,之前已经准备了半个多月,从7月底开始投简历面试,目前是jav ...

  6. java hibernate sqlserver自增_怎样在hibernate中实现oracle的主键自增策略?

    Oracle数据库索引和SQLServer的阐述 Oracle常见表结构 oracle数据库约束条件删除.取消.启用 在很多情况下,我们使用Hibernate在已经建立好数据库的基础上.在oracle ...

  7. mysql幻读和不可重复读的区别_面试官:MySQL的可重复读级别能解决幻读吗

    Java面试笔试面经.Java技术每天学习一点 Java面试 关注不迷路 作者:宁愿. 来源:https://juejin.im/post/5c9040e95188252d92095a9e 引言 之前 ...

  8. 不可重复读和幻读的区别_面试官:MySQL的可重复读级别能解决幻读吗

    Java面试笔试面经.Java技术每天学习一点 Java面试 关注不迷路 作者:宁愿. 来源:https://juejin.im/post/5c9040e95188252d92095a9e 引言 之前 ...

  9. mysql 分段执行_面试官问你MySQL的优化,看这篇文章就够了

    作者:zhangqh segmentfault.com/a/1190000012155267 一.EXPLAIN 做MySQL优化,我们要善用 EXPLAIN 查看SQL执行计划. 下面来个简单的示例 ...

最新文章

  1. halcon学习笔记——(5)HDevelop language(异常处理)
  2. 题目 1004: [递归]母牛的故事
  3. first-child和first-of-type的区别
  4. 前端学习(1435):vue能做什么
  5. Mybatis 批量操作总结
  6. python自动数据结构_Python 数据结构
  7. maven setting.xml详解
  8. 酷派大神F1-救砖详细教程! 酷派8297w(大神F1联通版) 恢复官方兼救砖
  9. 如何用Python进行数据分析
  10. java timeunit_java – 了解TimeUnit
  11. Unity3D性能优化——工具篇
  12. 一种基于暗通道先验算法的图像去雾技术研究
  13. PS制作透明的BMP图片
  14. 4种免费将图片转成PDF的方法,手机电脑都能用
  15. clr(clr a是什么指令)
  16. 基于ARM的移动视频监控系统
  17. “金融+科技”内外双赋能 拆解数字化浪潮下的平安打法
  18. 摆线方程推导(向量法)
  19. 想知道游戏建模师每天都在做什么吗?在工作中应具备哪些能力?
  20. Fecshop 环境部署以及安装步骤

热门文章

  1. 【PHP】xampp配置多个监听端口和不同的网站目录(转)
  2. C# Socket网络编程精华篇
  3. IIS6.0,Apache低版本,PHP CGI 解析漏洞
  4. Bootstrap富文本编辑器-bootstrap-wysiwyg
  5. jdk8永久代从方法区移除的验证
  6. C#使用多态求方形面积周长和圆的面积周长
  7. old header
  8. [转] SQL2000 关于 Java JDBC 驱动的安装和设定
  9. [pytorch、学习] - 5.9 含并行连结的网络(GoogLeNet)
  10. 软件测试工具LoadRunner中如何定义SLA?--转载