
MySQL主键自增值为什么有“空洞”?
• 1.场景准备
• 2.开始测试
• 3.问题分析
• 4.问题拓展
本文在测试 insert、insert ignore、replace into 三种数据插入方式的时候,发现插入数据的时候在表内存在带有“唯一特性”的值重复的情况下三种语句的处理方式。最终发现了MySQL主键自增值“空洞”了
1.场景准备
测试场景为MySQL 8.0:
• 主键重复场景
• 唯一键重复场景
1、建表,包含主键及唯一约束
2、写入初始测试数据
2.开始测试
insert into
insert ignore into
insert方式插入数据在处理过程中发生主键传统等错误时候,语句会被终止,并告知错误的原因。而使用insert ignore的方式进行数据插入,则会忽略插入错误的行继续插入没有问题的行记录,最终以warning进行提示。
在测试过程中惊奇的发现测试表中的主键自增列发生了改变,经过之前的操作已经变成了7:
replace into
最后,replace into的方式导致如果插入数据是原值的情况,然后主键冲突,就对该主键的内容进行替换,如果唯一键冲突,唯一值所在行就会删除,重新插入新的行,如果都不冲突则正常插入数据。
上文测试了三种插入数据的方式,可是测试过程中发现插入失败的时候,自增列的自增值居然变大了。
3.问题分析
为了更好地理解,首先让我们具体认识一下AUTO_INCREMENT属性在不同的存储引擎当中,其自增值的保存策略有所不同:
• MyISAM引擎的自增值是保存在数据文件中的。
• InnoDB引擎的自增值,其实是保存在了内存里,并且到了MySQL 8.0版本后,将自增值的变更记录在了redo log中,当MySQL发生重启的时候依靠redo log恢复重启之前的自增值。在此前,现在该表的自增值是7,重启后又变成4了。
可是理解了这个并不能马上理解现在的这个问题,我们知道当数据进行数据插入的时候,如果插入的数据中自增列不指定其值的时候,该列就会以当前自增值作为其值,如果指定其值就会插入指定的值,当然也有满足唯一的原则,同时插入指定值大于自增值时,自增值也会随之改变。而自增值使用的算法是以auto_increment_offset参数决定开始,以auto_increment_increment决定步长来实现的,默认情况都是1:
那么,为什么会出现插入数据未成功,自增值却变大了的情况呢?原因很简单,用插入数据的流程来进行分析:
因为自增值的保存是在插入数据真正执行前完成的,因此就会出现这种问题了。
这个时候有人就会想了,可以把AUTO_INCREMENT值改回去吗?简单测试一下:
显然,如果自增值往大的方向修改是没有问题的,但如果往小的修改就要看目前数据库插入的值是否会将修改后的自增值“卡”在中间,如果出现这种情况是没办法改回去的,原因显而易见,自增属性与主键配套使用,如果现在表里id=4和id=6之间差了个5的值,将自增值改回5,当插入数据时,自增值就会插入5的值并且把自增值加1,问题就出现了,此时自增值再进行插入就违背了唯一的原则了
4.问题拓展
在生产环境中还存在很多类似的问题,如:
在插入过程中,开启了一个事务,在插入的时候发生了事务的回滚,当回滚后再次插入数据,发现自增值又从出现了“空洞”,那么问题又来了,为什么在插入数据的时候发生了回滚,数据回滚了,自增值却没有回滚呢?为了更直观,继续测试,假设有两个事务。
测试前数据:
进行测试:
测试后数据:
发现还是“空洞”了,而且此时答案也十分清楚了,在不同事务在进行写入操作的时候申请自增值,为了避免两个事务申请到相同的自增值,所以需要对其加锁,按照一定顺序进行申请自增值。根据前面的例子:
• 首先两个session都开启了事务,session1前的是id=14的自增值,session2则申请到id=15的自增值
• 接着当session2插入成功后提交了事务,而此时,session1插入成功或出现插入失败时进行了事务回滚
此时就出现了前面说到的问题了,没办法回滚,回滚就会出现自增值“卡”在中间的情况了,以后有机会再继续聊聊自增锁的问题。
本文转载自公共号GreatSQL社区。
