MySQL浅析之自增主键是连续的吗(6)

MySQL浅析之自增主键是连续的吗

1. 前言

看了前面的博客,相信我们都知道了使用自增主键的好处:可以让主键索引尽量地保持递增顺序插入,避免页分裂。

但是避免页分裂的前提是自增主键必须是连续性的。

这时我们可能就有疑问了,难道自增主键还会出现不是连续的情况吗?

没错,自增主键是不能保证连续递增。

2. 自增值保存在哪?

要想了解自增主键为什么不能保证连续,我们就先要明白自增值是保存在哪的。

不妨我们直接看结论:

对于MyISAM引擎的子增值是保存在数据文件中的;而对于InnoDB引擎的自增值,早期是保存在内存里的,直到MySQL 8.0版本后才有了“自增值持久化”的能力。

MyISAM引擎暂且先不讨论,主要先了解下InnoDB引擎。

  • 对于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对自增值的保存策略。我们可以看出,貌似自增值是连续的,并没有我们所说的不连续情况。到底是什么回事呢,别急,我们接下来看看自增值修改机制。

3. 自增值修改机制

当主键ID被定义为AUTO_INCREMENT时,插入一条数据时,分为两种情况:

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

如果是第二种情况,那么插入的ID值不同,结果也不一样:

  • 如果插入的ID值大于等于自增值,就需要把当前自增值修改为新的自增值。
  • 如果插入的ID值小于自增值,那么这个表的自增值不变。

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

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

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

了解了这些后,我们再来讨论博客标题的问题。

4. 自增值的修改时机

要回答这个问题,我们先来看一下自增值的修改时机。

我们先创建一个表:

CREATE TABLE `table` (
  `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;

我们现在表中插入一条数据:

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

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

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

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

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

唯一键冲突是导致自增主键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是递增的,但不保证是连续的。

5. 小结

本篇博客讲述了“自增主键为什么会出现不连续的值”的问题,以及出现的两种情况:

  1. 唯一键冲突是导致自增主键id不连续的第一种原因。
  2. 事务回滚也会产生类似的现象,这就是第二种原因。

其实在实际开发中并不仅仅只有这两种情况,例如自增锁的优化也是会出现这种情况的。

其次还简单介绍了自增值保存的地方,以及MyISAM和InnoDB对于自增ID的区别。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值