mysql自增主键详解

2021/7/6 2:29:05

本文主要是介绍mysql自增主键详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 什么是自增主键?
    • 为什么要有自增主键?
    • 自增主键有哪些缺点?
    • 自增主键为什么不会导致页分裂?
    • 自增id用完怎么办?
  • InnoDB 引擎的自增值的保存策略
  • 自增主键为什么只递增不连续?
    • 原因一:插入失败
    • 原因二:事务回滚
    • 为什么不改回去呢?
    • 原因三:自增锁的优化

什么是自增主键?

当设置了主键自增时,主键的生成可以完全依赖数据库,无需人为干预,在新增数据的时候,我们不设置主键字段的值,数据库就会为我们自动生成一个主键值。

自增主键是整数型,单调递增,对创建索引也十分友好。不过只保证递增,不保证连续。

由于自增主键可以让主键索引保持递增插入,所以避免了页分裂,索引更紧凑

为什么要有自增主键?

1、性能更优
因为自增主键可以让主键索引保持递增顺序插入,因此避免了页分裂,索引更紧凑

为何递增插入就可避免页分裂?
因为递增插入的记录之间没有空隙,记录自增主键索引的一个页面满了,就申请另外一个页面,接着从左边开始写数据。
所以不会存在以下情况:当数据页已经满了,此时来了一个新记录,需要插入到数据页中间有缝隙的两个数据之间,造成页分裂

而如果是用业务逻辑的字段主键,则往往不容易保证有序递增插入,造成页分裂,导致写数据成本相对较高。

2、更省空间
除了考虑性能外,我们还可以从存储空间的角度来看。

假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?

由于每个二级索引都包含主键值,所以主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小,数据页能存的索引就越多。
自增主键用int型只需4字节,身份证号用字符型,一个数字一个字符至少3字节。导致二级索引占用空间更大。

所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。

那有没有什么场景适合用业务字段直接做主键的呢?
还是有的。比如有些业务的场景需求是这样的:只有一个索引且该索引是唯一索引。
由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
这时候我们就要优先考虑“尽量使用主键查询”原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。

自增主键有哪些缺点?

自增主键不利于数据迁移,比如将多个子系统的数据合并到一个数据库。

自增主键为什么不会导致页分裂?

其实按照传统B+树的插入规则,即使是自增插入,当一个数据页满的时候,也是会引起页分裂的。但是innodb在这一块做了优化
怎么优化的?
判断如果是自增插入且当前页已满的情况下,不改变原有页的结构,而是将新的数据放到一个新页中。
在innodb的实现中,为每个索引页面维护了一个上次插入的位置,以及上次的插入是递增/递减的标识
根据这些信息,innodb能够判断出新插入到页面中的记录,是否仍旧满足递增/递减的约束,若满足约束,则采用优化后的分裂策略;若不满足约束,则退回到传统的分裂策略

自增id用完怎么办?

MySQL 里有很多自增的 id,每个自增 id 都是定义了初始值,然后不停地往上加步长。虽然自然数是没有上限的,但是在计算机里,只要定义了表示这个数的字节长度,那它就有上限。

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

InnoDB 引擎的自增值的保存策略

在 MySQL 5.7 及之前的版本,自增值保存在内存里,并没有持久化。每次重启后,会将 max(id)+1 作为这个表当前的自增值

举例来说,如果一个表当前数据行里最大的 id 是 10,AUTO_INCREMENT=11。这时候,我们删除 id=10 的行,max(id)变为9,重启后这个表的 AUTO_INCREMENT 就会变成9+1=10

MySQL 8.0 版本后,才有了“自增值持久化”的能力,将自增值的变更记录在了 redo log 中,重启的时候依靠 redo log 恢复重启之前的值。使得自增值不随max(id)而减小。

自增主键为什么只递增不连续?

原因一:插入失败

假设,表 t 里面已经有了 (1,1,1) 这条记录,这时我再执行一条插入数据命令:

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

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

  • 执行器调用 InnoDB 引擎接口写入一行,传入的这一行的值是 (0,1,1);
  • InnoDB 发现用户没有指定自增 id 的值,获取表 t 当前的自增值 2;
  • 将插入行的值改成 (2,1,1);
  • 将表的自增值改成 3;
  • 继续执行插入数据操作,由于某些原因导致插入数据库失败。

可以看到,这个表的自增值改成 3,是在真正执行插入数据的操作之前。这个语句真正执行的时候失败,所以 id=2 这一行并没有插入成功,但也没有将自增值再改回去。
所以,在这之后,再插入新的数据行时,拿到的自增 id 就是 3。也就是说,出现了自增主键不连续的情况。

原因二:事务回滚

例如:

insert into t values(null,1,1);// 插入的行是 (1,2,2)
begin;
insert into t values(null,2,2); // 插入的行是 (2,2,2)
rollback;  //回滚导致1和2都没有了
insert into t values(null,2,2); // 插入的行是 (3,2,2) ,所以id从3开始不连续了、

为什么不改回去呢?

你可能会问,为什么在出现唯一键冲突或者回滚的时候,MySQL 没有把表 t 的自增值回退呢?

因为如果可以回退,则会造成主键冲突,如果要解决主键冲突就又会降低性能。
为了保证性能,InnoDB 选择了语句执行失败时不回退自增 id。所以才只保证了自增 id 是递增的,但不保证是连续的。

原因三:自增锁的优化

可以看到,自增 id 锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。
其实,在 MySQL 5.0 版本时,并不是这样的。

接下来,我会先给你介绍下自增锁设计的历史,这样有助于你分析接下来的一个问题。

在 MySQL 5.0 版本的时候,自增锁的范围是语句级别。也就是说,如果一个语句申请了一个表自增锁,这个锁会等语句执行结束以后才释放。显然,这样设计会影响并发度。

MySQL 5.1.22 版本引入了一个新策略,新增参数 innodb_autoinc_lock_mode,取值如下:

  • 这个参数的值被设置为 0 时,和以前一样,语句执行结束后才释放锁;

  • 这个参数的值被设置为 1 时:

    • 普通 insert 语句,自增锁在申请之后就马上释放;
    • 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
  • 这个参数的值被设置为 2 时,所有的申请自增主键的动作都是申请后就马上释放锁

你一定有两个疑问:为什么默认设置下,insert … select 要使用语句级的锁?为什么这个参数的默认值不是 2?

答案是,为了数据的一致性

我们一起来看一下这个场景:
图:批量插入数据的自增锁
在这里插入图片描述
在这个例子里,我往表 t1 中插入了 4 行数据,然后创建了一个相同结构的表 t2,然后两个 session 同时执行向表 t2 中插入数据的操作。

你可以设想一下,如果 session B 是申请了自增值以后马上就释放自增锁,那么就可能出现这样的情况:

  • session B 先插入了两个记录,(1,1,1)、(2,2,2);
  • 然后,session A 来申请自增 id 得到 id=3,插入了(3,5,5);
  • 之后,session B 继续执行,插入两条记录 (4,3,3)、 (5,4,4)。

你可能会说,这也没关系吧,毕竟 session B 的语义本身就没有要求表 t2 的所有行的数据都跟 session A 相同。

是的,从数据逻辑上看是对的。但是,如果我们现在的 binlog_format=statement,你可以设想下,binlog 会怎么记录呢?

由于两个 session 是同时执行插入数据命令的,所以 binlog 里面对表 t2 的更新日志只有两种情况:要么先记 session A 的,要么先记 session B 的。

但不论是哪一种,这个 binlog 拿去从库执行或者用来恢复临时实例,在备库和临时实例里面,session B 这个语句执行出来,生成的结果里面,id 都是连续的。这时,这个库就发生了数据不一致

出现这个问题的原因是什么?

其实,这是因为原库 session B 的 insert 语句,生成的 id 不连续。这个不连续的 id,用 statement 格式的 binlog 来串行执行,是执行不出来的。

如何解决这个问题?

有两种思路:

  1. 让原库的批量插入数据语句,固定生成连续的 id 值。所以,自增锁直到语句执行结束才释放,就是为了达到这个目的。

  2. 在 binlog 里面把插入数据的操作都如实记录进来,到备库执行的时候,不再依赖于自增主键去生成。这种情况,其实就是 innodb_autoinc_lock_mode 设置为 2,同时 binlog_format 设置为 row

生产环境建议:
在生产上,尤其是有 insert … select 这种批量插入数据的场景时,从并发插入数据性能的角度考虑,
建议这样设置:innodb_autoinc_lock_mode=2 ,并且 binlog_format=row.
这样做既能提升并发性,又不会出现数据一致性问题。

需要注意的是,我这里说的批量插入数据,包含的语句类型是 insert … select、replace … select 和 load data 语句。
但是,在普通的 insert 语句里面包含多个 value 值的情况下,即使 innodb_autoinc_lock_mode = 1,也不会等语句执行完成才释放锁。因为这类语句在申请自增 id 的时候,是可以精确计算出需要多少个 id 的,然后一次性申请,申请完成后锁就可以释放了。

也就是说,批量插入数据的语句,之所以需要这么设置,是因为“不知道要预先申请多少个 id”

既然预先不知道要申请多少个自增 id,那么一种直接的想法就是需要一个时申请一个。但如果一个 select … insert 语句要插入 10 万行数据,按照这个逻辑的话就要申请 10 万次。显然,这种申请自增 id 的策略,在大批量插入数据的情况下,不但速度慢,还会影响并发插入的性能。

因此,对于批量插入数据的语句,MySQL 有一个批量申请自增 id 的策略

  • 语句执行过程中,第一次申请自增 id,会分配 1 个;
  • 1 个用完以后,这个语句第二次申请自增 id,会分配 2 个;
  • 2 个用完以后,还是这个语句,第三次申请自增 id,会分配 4 个;
  • 依此类推,同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍

举个例子,我们一起看看下面的这个语句序列:

insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);
create table t2 like t;
insert into t2(c,d) select c,d from t;
insert into t2 values(null, 5,5);

insert…select,实际上往表 t2 中插入了 4 行数据。但是,这四行数据是分三次申请的自增 id,第一次申请到了 id=1,第二次被分配了 id=2 和 id=3, 第三次被分配到的id就是4 到7。

由于这条语句实际只用上了 4 个 id,所以 id=5 到 id=7 就被浪费掉了。之后,再执行 insert into t2 values(null, 5,5),实际上插入的数据就是(8,5,5)

这也就是主键 id 出现自增 id 不连续的第三种原因



这篇关于mysql自增主键详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程