目录
- 自增值保存在哪儿
- 自增值修改机制
- 自增值的修改时机
- 自增锁的优化
- 参考资料
这篇文章主要介绍 MySQL 的自增主键,由于自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑。但自增主键不能保证连续递增,why?接下来我们就来分析一下是什么原因。
自增值保存在哪儿
表的结构定义存放在后缀名为.frm的文件中,但是并不会保存自增值。不同的引擎对于自增值的保存策略不同。
- 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恢复重启之前的值。
自增值修改机制
如果字段id被定义为AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:
- 如果插入数据时id字段指定为0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT值填到自增字段;
- 如果插入数据时id字段指定了具体的值,就直接使用语句里指定的值。
根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是X,当前的自增值是Y。
- 如果X<Y,那么这个表的自增值不变;
- 如果X≥Y,就需要把当前自增值修改为新的自增值。
新的自增值生成算法是:从auto_increment_offset开始,以auto_increment_increment为步长,持续叠加,直到找到第一个大于X的值,作为新的自增值。
其中,auto_increment_offset 和 auto_increment_increment是两个系统参数,分别用来表示自增的初始值和步长,默认值都是1。
当auto_increment_offset和auto_increment_increment都是1的时候,新的自增值生成逻辑很简单,就是:
- 如果准备插入的值>=当前自增值,新的自增值就是“准备插入的值+1”;
- 否则,自增值不变。
在这两个参数都设置为1的时候,自增主键id却不能保证是连续的,这是什么原因呢?
自增值的修改时机
假设,表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(规定c唯一)的记录,所以报Duplicate key error,语句返回。
这个语句真正执行的时候,因为碰到唯一键c冲突,所以id=2这一行并没有插入成功,但也没有将自增值再改回去。所以,在这之后再插入新的数据行时,拿到的自增id就是3。也就是说,出现了自增主键不连续的情况。
唯一键冲突是导致自增主键id不连续的第一种原因。
**事务回滚也会产生类似的现象,这就是第二种原因。**自增值不能回退,这么设计是为了提升性能。
所以才只保证了自增id是递增的,但不保证是连续的。
自增锁的优化
自增id锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。
在MySQL 5.0版本的时候,自增锁的范围是语句级别。也就是说,如果一个语句申请了一个表自增锁,这个锁会等语句执行结束以后才释放。显然,这样设计会影响并发度。
MySQL 5.1.22版本引入了一个新策略,新增参数innodb_autoinc_lock_mode,默认值是1。
- 这个参数的值被设置为0时,表示采用之前MySQL 5.0版本的策略,即语句执行结束后才释放锁;
- 这个参数的值被设置为1时:
- 普通insert语句,自增锁在申请之后就马上释放;
- 类似insert … select这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
- 这个参数的值被设置为2时,所有的申请自增主键的动作都是申请后就释放锁。
因此,在生产上,尤其是有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个数都是上一次的两倍。
可能会造成多申请的自增id没用到,这是主键id出现自增id不连续的第三种原因。
参考资料
《MySQL 必知必会》
《MySQL 45讲》