热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

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

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

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

e1e1f4ce25d6b7402033862b4a11fd12.png

图 1 - MySQL 的主键

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

CREATE

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

  • 较早版本的 MySQL 将 AUTO_INCREMENT 存储在内存中,实例重启后会根据表中的数据重新设置该值;
  • 获取 AUTO_INCREMENT 时不会使用事务锁,并发的插入事务可能出现部分字段冲突导致插入失败;

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

删除记录

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

d16141742268167710ce59390bd1f822.png

图 2 - AUTO_INCREMENT 的使用

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

SELECT MAX(ai_col) FROM table_name FOR UPDATE;

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

88f5cf5a6f94f97b6bf5a1cd4b355174.png

图 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 的两条记录:

5283618f07f668f4acacdb2728aabc6e.png

图 4 - 并发事务的执行

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

16c2baa5ce3632d043eaeb8285e118ac.png

图 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 ... SELECT、REPLACE ... 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_increment 和 auto_increment_offset 是用来做什么的?

原文链接:https://www.toutiao.com/a6817982962324210183/



推荐阅读
  • 本文介绍 SQL Server 的基本概念和操作,涵盖系统数据库、常用数据类型、表的创建及增删改查等基础操作。通过实例帮助读者快速上手 SQL Server 数据库管理。 ... [详细]
  • 本文深入探讨了SQL数据库中常见的面试问题,包括如何获取自增字段的当前值、防止SQL注入的方法、游标的作用与使用、索引的形式及其优缺点,以及事务和存储过程的概念。通过详细的解答和示例,帮助读者更好地理解和应对这些技术问题。 ... [详细]
  • 优化Flask应用的并发处理:解决Mysql连接过多问题
    本文探讨了在Flask应用中通过优化后端架构来应对高并发请求,特别是针对Mysql 'too many connections' 错误的解决方案。我们将介绍如何利用Redis缓存、Gunicorn多进程和Celery异步任务队列来提升系统的性能和稳定性。 ... [详细]
  • 本文介绍了如何利用 Spring Boot 和 Groovy 构建一个灵活且可扩展的动态计算引擎,以满足钱包应用中类似余额宝功能的推广需求。我们将探讨不同的设计方案,并最终选择最适合的技术栈来实现这一目标。 ... [详细]
  • 本文详细介绍了优化DB2数据库性能的多种方法,涵盖统计信息更新、缓冲池调整、日志缓冲区配置、应用程序堆大小设置、排序堆参数调整、代理程序管理、锁机制优化、活动应用程序限制、页清除程序配置、I/O服务器数量设定以及编入组提交数调整等方面。通过这些技术手段,可以显著提升数据库的运行效率和响应速度。 ... [详细]
  • 本文深入探讨了MySQL中常见的面试问题,包括事务隔离级别、存储引擎选择、索引结构及优化等关键知识点。通过详细解析,帮助读者在面对BAT等大厂面试时更加从容。 ... [详细]
  • Netflix利用Druid实现高效实时数据分析
    本文探讨了全球领先的在线娱乐公司Netflix如何通过采用Apache Druid,实现了高效的数据采集、处理和实时分析,从而显著提升了用户体验和业务决策的准确性。文章详细介绍了Netflix在系统架构、数据摄取、管理和查询方面的实践,并展示了Druid在大规模数据处理中的卓越性能。 ... [详细]
  • 在项目部署后,Node.js 进程可能会遇到不可预见的错误并崩溃。为了及时通知开发人员进行问题排查,我们可以利用 nodemailer 插件来发送邮件提醒。本文将详细介绍如何配置和使用 nodemailer 实现这一功能。 ... [详细]
  • 采用IKE方式建立IPsec安全隧道
    一、【组网和实验环境】按如上的接口ip先作配置,再作ipsec的相关配置,配置文本见文章最后本文实验采用的交换机是H3C模拟器,下载地址如 ... [详细]
  • 目录一、salt-job管理#job存放数据目录#缓存时间设置#Others二、returns模块配置job数据入库#配置returns返回值信息#mysql安全设置#创建模块相关 ... [详细]
  • 实用正则表达式有哪些
    小编给大家分享一下实用正则表达式有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下 ... [详细]
  • 深入解析Serverless架构模式
    本文将详细介绍Serverless架构模式的核心概念、工作原理及其优势。通过对比传统架构,探讨Serverless如何简化应用开发与运维流程,并介绍当前主流的Serverless平台。 ... [详细]
  • 在高并发需求的C++项目中,我们最初选择了JsonCpp进行JSON解析和序列化。然而,在处理大数据量时,JsonCpp频繁抛出异常,尤其是在多线程环境下问题更为突出。通过分析发现,旧版本的JsonCpp存在多线程安全性和性能瓶颈。经过评估,我们最终选择了RapidJSON作为替代方案,并实现了显著的性能提升。 ... [详细]
  • JavaScript中的数组是数据集合的核心结构之一,内置了多种实用的方法。掌握这些方法不仅能提高开发效率,还能显著提升代码的质量和可读性。本文将详细介绍数组的创建方式及常见操作方法。 ... [详细]
  • 深入剖析JVM垃圾回收机制
    本文详细探讨了Java虚拟机(JVM)中的垃圾回收机制,包括其意义、对象判定方法、引用类型、常见垃圾收集算法以及各种垃圾收集器的特点和工作原理。通过理解这些内容,开发人员可以更好地优化内存管理和程序性能。 ... [详细]
author-avatar
辣妈最___潮
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有