自增主键为什么会不连续
业务上的错误做法:设计依赖于自增主键的连续性.
自增主键不连续的情况:
测试使用的表结构
CREATE TABLE `t` ( `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;
重启导致自增主键变化:
(admin@xx:5001)[jinhailan]>show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `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 DEFAULT CHARSET=utf8mb4 1 row in set (0.02 sec) (admin@xx:5001)[jinhailan]>insert into t values(null,1,1); Query OK, 1 row affected (0.17 sec) (admin@xx:5001)[jinhailan]>show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `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 AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec) (admin@xx:5001)[jinhailan]>delete from t; Query OK, 1 row affected (0.16 sec)
重启mysql
(admin@xx:5001)[jinhailan]>show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `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 DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec)
自增值保存在哪里?
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 恢复重启之前的值。
自增值修改机制
在 MySQL 里面,如果字段 id 被定义为 AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:
1.如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;
2.如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是 X,当前的自增值是 Y。
如果 X<Y,那么这个表的自增值不变;
如果 X≥Y,就需要把当前自增值修改为新的自增值。
新的自增值生成算法是:
从 auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 X 的值,作为新的自增值
auto_increment_offset 和 auto_increment_increment 是两个系统参数,分别用来表示自增的初始值和步长,默认值都是 1
备注:在一些场景下,使用的就不全是默认值。比如,双 M 的主备结构里要求双写(两个主都写入,不是从一个节点写)的时候,我们就可能会设置成 auto_increment_increment=2,让一个库的自增 id 都是奇数,另一个库的自增 id 都是偶数,避免两个库生成的主键发生冲突。
不连续场景一:唯一键冲突
以上两个参数都设置成1,还是会出现不连续的情况.
自增值的修改时机
(admin@xx:5001)[jinhailan]>select * from t; +----+------+------+ | id | c | d | +----+------+------+ | 1 | 1 | 1 | | 2 | 2 | 2 | +----+------+------+ (admin@xx:5001)[jinhailan]> show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `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 AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec) (admin@xx:5001)[jinhailan]>insert into t values(null,1,1); ERROR 1062 (23000): Duplicate entry '1' for key 'c' (admin@xx:5001)[jinhailan]> show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `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 AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec)
以上过程的执行流程:
执行器调用 InnoDB 引擎接口写入一行,传入的这一行的值是 (0,1,1);
InnoDB 发现用户没有指定自增 id 的值,获取表 t 当前的自增值 2;
将传入的行的值改成 (2,1,1);
将表的自增值改成 3;
继续执行插入数据操作,由于已经存在 c=1 的记录,所以报 Duplicate key error,语句返回。
从这个过程可以看出,在真正的数据插入之前,自增值已经加1,但是真正的数据插入的时候发现唯一键c冲突,插入失败,但是自增id的值已经更改不会改回去.
因此出现了自增id不连续的情况.
不连续场景二:事务回滚
(admin@xx:5001)[jinhailan]>insert into t values(null,1,1); Query OK, 1 row affected (0.00 sec) (admin@xx:5001)[jinhailan]>begin; Query OK, 0 rows affected (0.00 sec) (admin@xx:5001)[jinhailan]>insert into t values(null,2,2); Query OK, 1 row affected (0.01 sec) (admin@xx:5001)[jinhailan]>select * from t; +----+------+------+ | id | c | d | +----+------+------+ | 1 | 1 | 1 | | 2 | 2 | 2 | +----+------+------+ 2 rows in set (0.00 sec) (admin@xx:5001)[jinhailan]>rollback; Query OK, 0 rows affected (0.00 sec) (admin@xx:5001)[jinhailan]>select * from t; +----+------+------+ | id | c | d | +----+------+------+ | 1 | 1 | 1 | +----+------+------+ 1 row in set (0.00 sec) (admin@xx:5001)[jinhailan]>insert into t values(null,2,2); Query OK, 1 row affected (0.00 sec) (admin@xx:5001)[jinhailan]>select * from t; +----+------+------+ | id | c | d | +----+------+------+ | 1 | 1 | 1 | | 3 | 2 | 2 | +----+------+------+ 2 rows in set (0.00 sec)
Q:为什么rollback之后没有把t的自增值改回2呢?
自增值为什么不能回退,否则可能会发生插入数据时主键冲突.
而为了解决这个主键冲突,有两种方法:
1.每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。
2.把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。可见,这两个方法都会导致性能问题。造成这些麻烦的罪魁祸首,就是我们假设的这个“允许自增 id 回退”的前提导致的。
因此,InnoDB 放弃了这个设计,语句执行失败也不回退自增 id。也正是因为这样,所以才只保证了自增 id 是递增的,但不保证是连续的。
自增锁的优化
自增ID锁并不是一个事务锁,而是每次申请完后就马上释放,以便允许别的事务再申请.但是MySQL5.1之前的版本不是如此
自增锁设计历史: