MySQL原理与实践(六):自增主键的使用

(尊重劳动成果,转载请注明出处:https://yangwenqiang.blog.csdn.net/article/details/91477092冷血之心的博客)

关注微信公众号(文强的技术小屋),学习更多技术知识,一起遨游知识海洋~

MySQL原理与实践(六):自增主键的使用_第1张图片

快速导航:

 MySQL原理与实践(一):一条select语句引出Server层和存储引擎层

MySQL原理与实践(二):一条update语句引出MySQL日志系统

MySQL原理与实践(三):由三种数据结构引入MySQL索引及其特性

MySQL原理与实践(四):由数据库事务引出数据库隔离级别

MySQL原理与实践(五):数据库的锁机制

MySQL原理与实践(六):自增主键的使用

目录

前言:

正文:

自增主键:

自增值的保存策略:

自增值的修改机制:

自增主键为什么不连续?

自增主键的修改时机:

事务的回滚操作对于自增主键连续性的影响:

自增值为什么不可以回退?

总结:

自增id用完了怎么办?

总结:


前言:

        在前面文章的介绍中,我们阐述了主键的重要性。我们通过普通索引查询时会利用主键做一个回表操作。当我们在建表的时候没有指定主键的时候,MySQL会自动帮我们创建一个rowid做为主键,由此可见主键的重要性。这篇博文中,我们主要分析总结自增主键的特性。

正文:

自增主键:

       让我们来回忆下前边所介绍的主键相关特性。在MySQL原理与实践(三):由三种数据结构引入MySQL索引及其特性中介绍索引的时候,我给出了如下的两个索引树:

MySQL原理与实践(六):自增主键的使用_第2张图片

由于数据的无序插入会导致数据页的分裂,增加了索引的维护成本,所以我们建议尽量使用自增主键来保证插入数据的有序性。在阿里巴巴的Java开发手册中也有如下的要求:

MySQL原理与实践(六):自增主键的使用_第3张图片

这一条强制规定也说明了自增主键的重要性,在MySQL中我们使用auto_increment来标明某个字段是个自增长的主键。有如下的建表语句:

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和其表结构如下:

MySQL原理与实践(六):自增主键的使用_第4张图片MySQL原理与实践(六):自增主键的使用_第5张图片

在这个空表 t 里面执行 insert into t values(null, 1, 1); 插入一行数据,再执行 show create table 命令,结果如下:

MySQL原理与实践(六):自增主键的使用_第6张图片

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

自增值的保存策略:

不同的引擎对于自增值的保存策略不同,我们以MyISAM和InnoDB存储引擎来说明:

  • 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 对自增值的保存策略以后,我们再看看自增值修改机制。

自增值的修改机制:

还是刚刚建立得表t,刚刚我们插入了一条(null,1,1)数据,查询select之后如下所示:

MySQL原理与实践(六):自增主键的使用_第7张图片

然后我们再插入一条(0,2,1)数据,查询结果如下:

MySQL原理与实践(六):自增主键的使用_第8张图片

然后我们不指主键id的值,插入了(3,1)数据,结果如下;

MySQL原理与实践(六):自增主键的使用_第9张图片

接着我们刻意指定了该主键id的值,插入了一条(5,5,1)数据,结果如下:

MySQL原理与实践(六):自增主键的使用_第10张图片

由上边的结果我们可以看出在 MySQL 里面,如果字段 id 被定义为 AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:

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

接着,我们再次执行show create table t\G 查看当前的自增值:

MySQL原理与实践(六):自增主键的使用_第11张图片

可以看到,由于我们在上一条插入语句中直接指定了自增值为5,所以MySQL保存的自增值大小为6 。

有如下的规定:

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

  • 如果 X
  • 如果 X≥Y,就需要把当前自增值修改为新的自增值。

接下来我们再次插入一条数据(null,5,1)因为列c上有唯一约束UNIQUE,所以会报错,如下所示:

MySQL原理与实践(六):自增主键的使用_第12张图片

然后,插入一条(null,6,1),看下结果:

MySQL原理与实践(六):自增主键的使用_第13张图片

可以看出,即使我们使用自增长的主键,并且默认步长为一,也会出现自增主键不连续的情况。那么为什么会出现这种情况呢?

自增主键为什么不连续?

自增主键的修改时机:

假设,表 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。也就是说,出现了自增主键不连续的情况。

事务的回滚操作对于自增主键连续性的影响:

      我们再来看一下事务的回滚操作对于自增主键连续性的影响。有如下的语句:

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 的一行数据了吗?

自增值为什么不可以回退?

       我们来分析这个设计思路,看看自增值为什么不能回退?

       假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。自增 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 是递增的,但不保证是连续的。

总结:

造成自增主键不连续的原因如下:

  • 唯一键的冲突
  • 事务的回滚操作

自增id用完了怎么办?

        前面我们主要介绍了自增id的特点和重要性。每个自增 id 都是定义了初始值,然后不停地往上加步长。虽然自然数是没有上限的,但是在计算机里,只要定义了表示这个数的字节长度,那它就有上限。比如,无符号整型 (unsigned int) 是 4 个字节,上限就是2的32次方-1。

        做为一个7*24小时提供服务的数据库,这个值是有可能被用完的,无非就是一个时间问题。那么自增id值用完会发生什么?我们以下列的语句来进行测试。

create table t(id int unsigned auto_increment primary key) auto_increment=4294967295;
insert into t values(null);
select * from t;
// 成功插入一行 4294967295
show create table t;

insert into t values(null);
//Duplicate entry '4294967295' for key 'PRIMARY'

结果如下:

MySQL原理与实践(六):自增主键的使用_第14张图片

       我们可以看出,可以看到,第一个 insert 语句插入数据成功后,这个表的 AUTO_INCREMENT 没有改变(还是 4294967295),就导致了第二个 insert 语句又拿到相同的自增 id 值,再试图执行插入语句,报主键冲突错误。

      4294967295不是一个特别大的数,对于一个频繁插入删除数据的表来说,是可能会被用完的。因此在建表的时候你需要考察你的表是否有可能达到这个上限,如果有可能,就应该创建成 8 个字节的 bigint unsigned。

表定义的自增值达到上限后的逻辑是:再申请下一个 id 时,得到的值保持不变。

       文章的开头,我们说了如果你创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id。InnoDB 维护了一个全局的 dict_sys.row_id 值,所有无主键的 InnoDB 表,每插入一行数据,都将当前的 dict_sys.row_id 值作为要插入数据的 row_id,然后把 dict_sys.row_id 的值加 1。

       实际上,在代码实现时 row_id 是一个长度为 8 字节的无符号长整型 (bigint unsigned)。但是,InnoDB 在设计时,给 row_id 留的只是 6 个字节的长度,这样写到数据表中时只放了最后 6 个字节,所以 row_id 能写到数据表中的值,就有两个特征:

  • row_id 写入表中的值范围,是从 0 到 2的48次方-1
  • 当 dict_sys.row_id=2的48次方时,如果再有插入数据的行为要来申请 row_id,拿到以后再取最后 6 个字节的话就是 0。

也就是说,写入表的 row_id 是从 0 开始到 2的48次方-1。达到上限后,下一个值就是 0,然后继续循环。

        当然,2的48次方-1 这个值本身已经很大了,但是如果一个 MySQL 实例跑得足够久的话,还是可能达到这个上限的。在 InnoDB 逻辑里,申请到 row_id=N 后,就将这行数据写入表中;如果表中已经存在 row_id=N 的行,新写入的行就会覆盖原有的行。

       从这个角度看,我们还是应该在 InnoDB 表中主动创建自增主键。因为,表自增 id 到达上限后,再插入数据时报主键冲突错误,是更能被接受的。毕竟覆盖数据,就意味着数据丢失,影响的是数据可靠性;报主键冲突,是插入失败,影响的是可用性。而一般情况下,可靠性优先于可用性。

总结:

        这篇文章中,我们介绍了自增主键的特性,包括自增主键为什么不是连续的,分析了唯一键冲突以及事务回滚都会导致其不连续。在文章的最后,还分析了主动创建的自增主键和InnoDB自动创建的自增主键rowid在自增值用完之后的表现,给出我们应该主动创建自增主键。

        这篇文章是我们MySQL原理与实践的第六篇,如果有机会的话,我会继续更新MySQL原理与实践的相关总结与笔记,欢迎大家关注交流,希望对大家的学习有所帮助。

 

如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,可以进群366533258一起交流学习哦~

本群给大家提供一个学习交流的平台,内设菜鸟Java管理员一枚、精通算法的金牌讲师一枚、Android管理员一枚、蓝牙BlueTooth管理员一枚、Web前端管理一枚以及C#管理一枚。欢迎大家进来交流技术。

关注微信公众号(文强的技术小屋),学习更多技术知识,一起遨游知识海洋~

MySQL原理与实践(六):自增主键的使用_第15张图片

你可能感兴趣的:(MySQL原理与实践)