大家好,我是小龙。之前阿里问过我这个问题:面试官:“你平时是怎么使用MySQL主键的?”我想:“咦,面试官一定想问InnoDB引擎的索引特性!”小龙:“我一般用主键自增主键!因为自增ID是有序的,会按顺序插到最后,而UUID是乱序的,随机生成,随机插入,这样会造成频繁分页,内存碎片,大量随机IO,心想:“这一波稳了!”然后,面试官又来了一套组合拳,面试官:“恩!好吧,那你知道自增主键是不是严格自增吗?”心想:“这不简单,一定是自增啊!不,仔细想想。自增主键在某些情况下是断线的,所以“小龙:”‘一定不能自增!’我以为到这里就结束了,只是为了测试一下自己是否真的用过,不想想还是天真面试官:“那你知道为什么不是严格递增的吗?换句话说,为什么不是连续的?”往下看吧!我要骂完了这个话题!不过既然其他问题都可以回答,那我还是pass吧!不过既然遇到了这个问题,就下来查资料了,还是要分享给大家!1.上一篇文章众所周知,自增主键让主键索引尽可能按升序插入,避免分页和大量随机IO。自增主键是不连续的,这是大家已经很熟悉的一个知识点,但是可能大部分小伙伴不知道为什么自增主键不是像之前的小龙那样严格自增的呢?在今天的文章中,下面说说这个问题,看看自增主键在什么情况下会出现“故障”?索引,然后插入一段data,然后我们看一下它的表结构。创建表`NewTable`(`id`int(11)NOTNULLAUTO_INCREMENT,`a`int(11)NULL,`b`int(11)NULL,PRIMARYKEY(`id`),UNIQUEINDEX`a`(`a`)USINGBTREE);insertintoxl_tbvalues(null,1,1)mysql>showcreatetablexl_tb\G;******************************1.row**************************表:xl_tbCreateTable:创建表`xl_tb`(`id`int(11)NOTNULLAUTO_INCREMENT,`a`int(11)DEFAULTNULL,`b`int(11)DEFAULTNULL,PRIMARYKEY(`id`),UNIQUEKEY`a`(`a`)USINGBTREE)ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf81rowinset(0.04sec)可以看到里面有个AUTO_INCREMENT=4表定义,表示下次插入数据时,如果需要自动生成自增值,则生成id=4。您也可以手动尝试!不过看到这里,可能有小伙伴会认为自增值存在于表结构中!哈哈,你这么想就错了!2、为什么自增主键不连续2.1.自增主键存储策略接下来我们来看看自增存储存储在什么地方!事实上,不同的存储引擎有不同的自增存储策略。MyISAM引擎的自增值保存在数据文件中。InnoDB引擎的自增值其实是保存在内存中的,而MySQL8.0之后,具备了“自增持久化”的能力,即“如果发生重启,表的自增值可以恢复到MySQL重启前的值”,具体情况是:在MySQL5.7及更早版本中,自增值是保存在内存中的,不持久化。每次重启后,第一次打开表时,会找到自增max(id)的最大值,然后将max(id)+1作为表的当前自增值。比如某表当前数据行最大id为10,则AUTO_INCREMENT=11。此时我们删除id=10的行,AUTO_INCREMENT还是11。但是如果立即重启实例,重启后这张表的AUTO_INCREMENT会变成10。?也就是说,MySQL重启可能会修改一个表的AUTO_INCREMENT的值。在MySQL8.0版本中,自增值的变化记录在redolog中,重启时依赖redolog恢复重启前的值。了解了MySQL对自增的保存策略,我们再来看看自增修改机制。2.2.自增修改机制如果插入数据时id字段指定为0、null或未指定的值,则将表当前的AUTO_INCREMENT值填入自增字段;如果id字段在插入数据时指定了具体值,则直接使用语句中指定的值。2.3.自增机制如果要插入的值>=当前的自增值,则新的自增值为“要插入的值+1”;否则,自增值不变。2.4.自增修改时机eg:假设表xl_tb中已经有记录(1,1,1),那么我再执行一条插入数据的命令:insertintotvalues(null,1,1);(自增id,uniquekeya,commonfieldb)这条语句的执行流程是:执行者调用InnoDB引擎接口写入一行,传入的这一行值为(0,1,1);InnoDB发现用户没有指定自增id值,获取表xl_tb4当前的自增值;将传入行的值更改为(2,1,1);将表的自增值改为5;继续插入数据操作,因为已经有a=1条记录,所以报Duplicatekey错误,语句返回。在真正执行插入数据操作之前,将这张表的自增值改为5。真正执行这条语句的时候,因为和唯一键a冲突,所以id=2行没有插入成功,但是自增值也没有改回来。因此,之后插入新的数据行时,得到的自增id为5。也就是说,存在自增主键不连续的情况。所以唯一键冲突是自增主键id不连续的第一个原因。同样,事务回滚也会产生类似的现象,这是第二个原因。这时候你可能会想,为什么mysql在唯一键冲突或者回滚的时候,不把xl_tb表的自增值改回来呢?如果把表xl_tb当前的自增值从5改成4,再插入新的数据,不就生成了一行id=2的数据吗?那么,按照我的思路,看看为什么不把自增主键放回去!首先我们假设有两个并行执行的事务A,B,在申请自增的时候,为了防止两个事务申请同一个自增id,必须加锁,然后在申请顺序。首先,事务A申请id=2,此时当前自增值为3,由于锁单申请,事务B申请id=3(当前自增值),此时当前自增值变为3+1=4那么事务A和B都插入,假设事务B先插入然后插入成功,然后事务A插入唯一键冲突。如果假设允许自增值退回,那么自增值会变成2。如果事务A继续插入,申请id=2,插入成功,申请id=3,插入,因为之前的事务B插入了id=3的数据,此时如何解决主键冲突?每次申请id之前,先判断表中是否已经存在这个id。扩大锁范围,必须等待事务执行完才能申请下一个。这两种方式虽然可以解决问题,但是性能无疑是极低的。所以不能回滚自增值,避免出现主键冲突等问题。(可能还有其他问题我没有想到)3.总结为什么自增主键不连续?MySQL5.7及更早版本,自增值保存在内存中,没有持久化事务回滚(自增值不能回滚,因为并发插入数据时,回滚自增ID可能会导致primarykeyconflicts)uniquekeyconflicts(因为表的自增值发生了变化,但是主键冲突没有插入,下次插入主键=变化的子自增值+1,所以是不连续)好了,今天的分享到此结束!有问题欢迎后台留言,或加入技术交流群一起讨论学习!本文转载自微信公众号「小龙coding」,可通过以下二维码关注。转载本文请联系小龙编码公众号。
