自增主键为什么会不连续

业务上的错误做法:设计依赖于自增主键的连续性.

自增主键不连续的情况:

测试使用的表结构

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之前的版本不是如此

自增锁设计历史:

 

posted @ 2019-10-23 11:01  asea金海兰  阅读(1052)  评论(0编辑  收藏  举报