zoukankan      html  css  js  c++  java
  • 自增主键为什么会不连续

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

    自增主键不连续的情况:

    测试使用的表结构

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

    自增锁设计历史:

  • 相关阅读:
    数据结构——单链表(singly linked list)
    Java——判断回文
    C——swap
    Java动态数组
    mui框架下监听返回按钮
    Ubuntu 18.04版本下安装网易云音乐
    Linux安装Broadcom无线驱动
    EFI环境下的Ubuntu&Win10双系统安装
    Leaflet中添加的不同图层样式图标
    数据插入数据库时,提示表名不存在
  • 原文地址:https://www.cnblogs.com/asea123/p/11725099.html
Copyright © 2011-2022 走看看