我们提到过自增主键,由于自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑。

之前我见过有的业务设计依赖于自增主键的连续性,也就是说,这个设计假设自增主键是连续的。但实际上,这样的假设是错的,因为自增主键不能保证连续递增

今天这篇文章,我们就来说说这个问题,看看什么情况下自增主键会出现 “空洞”?

为了便于说明,我们创建一个表 t,其中 id 是自增主键字段、c 是唯一索引


CREATE TABLE `t` (`id` int(11) NOT NULL AUTO_INCREMENT,`c` int(11) DEFAULT NULL,`d` int(11) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB;

自增值保存在哪儿?

在这个空表 t 里面执行

 insert into t values(null, 1, 1);

插入一行数据,再执行

 show create table t

命令,就可以看到如下图所示的结果:

Table   Create Table
------  ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
t       CREATE TABLE `t` (                                                                                                                                                                                                   `id` int(11) NOT NULL AUTO_INCREMENT,                                                                                                                                                                              `c` int(11) DEFAULT NULL,                                                                                                                                                                                          `d` int(11) DEFAULT NULL,                                                                                                                                                                                          PRIMARY KEY (`id`),                                                                                                                                                                                                UNIQUE KEY `c` (`c`)                                                                                                                                                                                               ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8         

可以看到,表定义里面出现了一个 AUTO_INCREMENT=2,表示下一次插入数据时,如果需要自动生成自增值,会生成 id=2。

其实,这个输出结果容易引起这样的误解:自增值是保存在表结构定义里的。实际上,表的结构定义存放在后缀名为.frm 的文件中,但是并不会保存自增值

不同的引擎对于自增值的保存策略不同。

MyISAM 引擎的自增值保存在数据文件中。

InnoDB 引擎的自增值,其实是保存在了内存里,并且到了 MySQL 8.0 版本后,才有了“自增值持久化”的能力,也就是才实现了“如果发生重启,表的自增值可以恢复为 MySQL 重启前的值”,具体情况是:

  • 在 MySQL 5.7 及之前的版本,自增值保存在内存里,并没有持久化。每次重启后,第一次打开表的时候,都会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值。

举例来说,如果一个表当前数据行里最大的 id 是 10,AUTO_INCREMENT=11。这时候,我们删除 id=10
的行,AUTO_INCREMENT 还是 11。但如果马上重启实例,重启后这个表的 AUTO_INCREMENT 就会变成
10。也就是说,MySQL 重启可能会修改一个表的 AUTO_INCREMENT 的值。

  • 在 MySQL 8.0 版本,将自增值的变更记录在了 redo log 中,重启的时候依靠 redo log 恢复重启之前的值。

理解了 MySQL 对自增值的保存策略以后,我们再看看自增值修改机制。

自增值修改机制

在 MySQL 里面,如果字段 id 被定义为 AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:

1 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;
2 如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。

根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是 X,当前的自增值是 Y。

新的自增值生成算法是:从 auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 X 的值,作为新的自增值。

中,auto_increment_offset 和 auto_increment_increment 是两个系统参数,分别用来表示自增的初始值和步长,默认值都是 1。

auto_increment_offsetauto_increment_increment 都是 1 的时候,新的自增值生成逻辑很简单,就是:

如果准备插入的值 >= 当前自增值,新的自增值就是“准备插入的值 +1”;
否则,自增值不变。

这就引入了我们文章开头提到的问题,在这两个参数都设置为 1 的时候,自增主键 id 却不能保证是连续的,这是什么原因呢?

自增值的修改时机

要回答这个问题,我们就要看一下自增值的修改时机。假设,表 t 里面已经有了 (1,1,1) 这条记录,这时我再执行一条插入数据命令:


insert into t values(null, 1, 1); 

这个语句的执行流程就是:

执行器调用 InnoDB 引擎接口写入一行,传入的这一行的值是 (0,1,1);
InnoDB 发现用户没有指定自增 id 的值,获取表 t 当前的自增值 2;
将传入的行的值改成 (2,1,1);
将表的自增值改成 3;
继续执行插入数据操作,由于已经存在 c=1 的记录,所以报 Duplicate key error,语句返回。

对应的执行流程图如下:

可以看到,这个表的自增值改成 3,是在真正执行插入数据的操作之前。这个语句真正执行的时候,因为碰到唯一键 c 冲突,所以 id=2 这一行并没有插入成功,但也没有将自增值再改回去

所以,在这之后,再插入新的数据行时,拿到的自增 id 就是 3。也就是说,出现了自增主键不连续的情况。

可以看到,这个操作序列复现了一个自增主键 id 不连续的现场 (没有 id=2 的行)。可见,唯一键冲突是导致自增主键 id 不连续的第一种原因

同样地,事务回滚也会产生类似的现象,这就是第二种原因。下面这个语句序列就可以构造不连续的自增 id,你可以自己验证一下。


insert into t values(null,1,1);
begin;
insert into t values(null,2,2);
rollback;
insert into t values(null,2,2);
//插入的行是(3,2,2)

你可能会问,为什么在出现唯一键冲突或者回滚的时候,MySQL 没有把表 t 的自增值改回去呢?如果把表 t 的当前自增值从 3 改回 2,再插入新数据的时候,不就可以生成 id=2 的一行数据了吗?

其实,MySQL 这么设计是为了提升性能。接下来,我就跟你分析一下这个设计思路,看看自增值为什么不能回退。

假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。

1 假设事务 A 申请到了 id=2, 事务 B 申请到 id=3,那么这时候表 t 的自增值是 4,之后继续执行。
2 事务 B 正确提交了,但事务 A 出现了唯一键冲突。
3 如果允许事务 A 把自增 id 回退,也就是把表 t 的当前自增值改回 2,那么就会出现这样的情况:表里面已经有 id=3 的行,而当前的自增 id 值是 2。
4 接下来,继续执行的其他事务就会申请到 id=2,然后再申请到 id=3。这时,就会出现插入语句报错“主键冲突”。

而为了解决这个主键冲突,有两种方法:

1 每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。

2 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。

可见,这两个方法都会导致性能问题。造成这些麻烦的罪魁祸首,就是我们假设的这个“允许自增 id 回退”的前提导致的。

因此,InnoDB 放弃了这个设计,语句执行失败也不回退自增 id。也正是因为这样,所以才只保证了自增 id 是递增的,但不保证是连续的

总结

  • 自增主键的设计不保证连续

思考

什么时候自增主键不连续?

自增主键为什么不是连续的?相关推荐

  1. 阿里二面差点败在这道题:MySQL自增主键为何不是连续的呢?

    面试官:"MySQL主键你一般怎样用?" 心想:"嘿嘿,面试官肯定想问InnoDB引擎索引特性相关知识!" 小龙:"平时主键我一般用自增主键!因为自增 ...

  2. mysql自增主键为什么不是连续的?

    我们在建表的时候,通常会加一个id的主键,并将它设置为AUTO_INCREMENT,很明显这个id是自增的,那么自增的id是不是就是连续的呢?先说结论,自增主键不能保证连续递增:那么什么什么情况下自增 ...

  3. 39 | MySQL的id自增主键一定都是连续的?(写的不错~)

    一.前言 先上一个表结构,后面例子就是依赖这个表数据 CREATE TABLE `t` (`id` int(11) NOT NULL AUTO_INCREMENT,`c` int(11) DEFAUL ...

  4. 你好,面试官 | 你觉得MySQL自增主键连续吗?

    面试现场 叮叮叮...... 面试官:"你好,我是XX面试官,请问是小龙吗?" 小龙:"您好,面试官,我是小龙" 面试官:"好的,现在有空吗,我们开始 ...

  5. mysql 主键冲突 多个事务回滚_MySQL实战45讲Day38----自增主键不是连续的原因

    一.自增主键不连续的原因: 对于表: CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `c` int(11) DEFAULT NULL ...

  6. 美团面试:MySQL 自增主键一定是连续的吗?

    美团问数据库比较多,分享一位读者面试美团遇到的关于 MySQL 自增主键的问题. 下面是正文. 众所周知,自增主键可以让聚集索引尽量地保持递增顺序插入,避免了随机查询,从而提高了查询效率 但实际上,M ...

  7. MySQL自增主键一定是连续的吗

    测试环境: MySQL版本:8.0 数据库表:T (主键id,唯一索引c,普通字段d) 如果你的业务设计依赖于自增主键的连续性,这个设计假设自增主键是连续的.但实际上,这样的假设是错的,因为自增主键不 ...

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

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

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

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

最新文章

  1. 免费直播:主流深度框架对比:总有一款适合你~
  2. Android自定义Seekbar拖动条式样
  3. 大量词云出现的时候IO的负担很大
  4. 10种常用降维算法源代码(python)
  5. Python Django开发案例:GET方式实现登录功能
  6. OpenGL编程轻松入门(一)
  7. P6113-[模板]一般图最大匹配【带花树】
  8. Redis数据库学习笔记
  9. python怎么读写_python怎么读写文件
  10. About static contructor API changes in cocos2d-...
  11. ASP.NET母版页和内容页之间如何互相传值?
  12. Wide character in print at ../lib/MonWalkProc.pm line 569.
  13. jquery ajax 向spring MVC controller中传值并接受及解析返回值
  14. C#属性默认值设置(model实体类)
  15. 阿里云短信发送SDK
  16. S200XD高拍仪读取身份证信息
  17. 国内主流集群渲染管理软件介绍
  18. 直播带货系统的六大热门功能模块概述
  19. Flutter 使用阿里巴巴icon库
  20. eclipse官网32位下载地址

热门文章

  1. MegaRAID Storage Manager RAID管理工具实用教程
  2. c#连接kafka_c#操作kafka(上)搭建kafka环境
  3. diff命令两个服务器文件,LINUX命令diff-文件管理-比较给定的两个文件的不同
  4. c调用python函数_python - Linux C调用Python 函数
  5. Linux 下mysql操作
  6. java死锁怎么用jvm调试_jvm 内存dump、gc查看、线程死锁,jmap、jstack、jstat
  7. 使用tcl 创建vivado工程
  8. python测试代码运行时间_在Python中准确地测量代码执行时间
  9. matlab表格三维柱状图,excel制作四维数据表格-excel三维柱形图 ,请问如何根据excel表格中的数据......
  10. php 5.6.6.tar.gz.asc,Signature Checking Using GnuPG