• memory引擎+自增值非连续原因


    Memory引擎

    create table t1(id int primary key, c int) engine=Memory;
    create table t2(id int primary key, c int) engine=innodb;
    insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
    insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
    

    !

    innodb主键索引上的值是有序存储的,在执行select * 时,就会按照叶子节点从左到右扫描,得到的结果就是0在第一行。而memory的数据部分以数组部分单独存放,在主键id索引里,存的是数据的位置,主键id是hash索引,key并不是有序的。

    • InnoDB引擎把数据放在主键索引上,其他索引上保存的是主键id。这种方式,我们称之为索引组织表(Index Organizied Table)。
    • 而Memory引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表(Heap Organizied Table)。

    两者不同之处

    1. InnoDB表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的;
    2. 当数据文件有空洞的时候,InnoDB表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;
    3. 数据位置发生变化的时候,InnoDB表只需要修改主键索引,而内存表需要修改所有索引;
    4. InnoDB表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的。
    5. InnoDB支持变长数据类型,不同记录的长度可能不同;内存表不支持Blob 和 Text字段,并且即使定义了varchar(N),实际也当作char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。

    由于内存表的这些特性,每个数据行被删除以后,空出的这个位置都可以被接下来要插入的数据复用。

    delete from t1 where id=5;
    insert into t1 values(10,10);
    select * from t1;
    

    就会看到返回结果里,id=10这一行出现在id=4之后,也就是原来id=5这行数据的位置。

    hash索引和B-Tree索引

    alter table t1 add index a_btree_index using btree (id);
    

    在内存表上也是支持B-Tree索引的。

    • 锁粒度问题:

      内存表不支持行锁,只支持表锁。因此,一张表只要有更新,就会堵住其他所有在这个表上的读写操作。

    • 数据持久化的问题:

      数据放在内存中,是内存表的优势,但也是一个劣势。因为,数据库重启的时候,所有的内存表都会被清空。

    内存表并不适合在生产环境上作为普通数据表使用。

    1. 如果你的表更新量大,那么并发度是一个很重要的参考指标,InnoDB支持行锁,并发度比内存表好;
    2. 能放到内存表的数据量都不大。如果你考虑的是读的性能,一个读QPS很高并且数据量不大的表,即使是使用InnoDB,数据也是都会缓存在InnoDB Buffer Pool里的。因此,使用InnoDB表的读性能也不会差。

    在数据量可控,不会耗费过多内存的情况下,你可以考虑使用内存表。内存临时表刚好可以无视内存表的两个不足,主要是下面的三个原因:

    1. 临时表不会被其他线程访问,没有并发性问的问题
    2. 临时表重启后也需要删除的,
    3. 备库的临时表不会影响主库的用户线程。

    例子:

    create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
    insert into temp_t select * from t2 where b>=1 and b<=2000;
    select * from t1 join temp_t on (t1.b=temp_t.b);
    
    1. 相比于InnoDB表,使用内存表不需要写磁盘,往表temp_t的写数据的速度更快;
    2. 索引b使用hash索引,查找的速度比B-Tree索引快;
    3. 临时表数据只有2000行,占用的内存有限。

    小结:由于在锁粒度和数据持久化方面的限制,memory内存表不适合做普通表的索引表和数据表,但是在内存临时表中,可以忽略这些问题,对业务不会造成很大影响,为了提高读写性能和联表速度,内存临时表都会比innodb临时表要快。

    自增主键为什么不是连续的

    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;
    

    在MySQL里面,如果字段id被定义为AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:

    1. 如果插入数据时id字段指定为0、null 或未指定值,那么就把这个表当前的
      AUTO_INCREMENT值填到自增字段;
    2. 如果插入数据时id字段指定了具体的值,就直接使用语句里指定的值。

    根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是X,当前的自增值是Y。

    1. 如果X<Y,那么这个表的自增值不变;
    2. 如果X≥Y,就需要把当前自增值修改为新的自增值。

    新的自增值生成算法是:从auto_increment_offset开始,以auto_increment_increment为步长,持续叠加,直到找到第一个大于X的值,作为新的自增值。
    其中,auto_increment_offset 和 auto_increment_increment是两个系统参数,分别用来表示自增的初始值和步长,默认值都是1。

    • 备注:在一些场景下,使用的就不全是默认值。比如,双M的主备结构里要求双写的时候,我们就可能会设置成auto_increment_increment=2,让一个库的自增id都是奇数,另一个库的自增id都是偶数,避免两个库生成的主键发生冲突。

    自增值的修改时机

    假设,表t里面已经有了(1,1,1)这条记录,这时我再执行一条插入数据命令:

    insert into t values(null, 1, 1);
    

    这个语句执行流程:

    1. 执行器调用innodb引擎写入一行,传入的值是(0,1,1)
    2. innodb发现用户没有指定自增的id值,获取当前表t的自增值是2
    3. 将传入的行的值改为(2,1,1)
    4. 将表的自增值改为3,
    5. 继续执行执行插入数据操作

    可以看到,这个表的自增值改成3,是在真正执行插入数据的操作之前。这个语句真正执行的时候,因为碰到唯一键c冲突,所以id=2这一行并没有插入成功,但也没有将自增值再改回去。

    可以看到,这个操作序列复现了一个自增主键id不连续的现场(没有id=2的行)。可见,唯一键冲突是导致自增主键id不连续的第一种原因。

    insert into t values(null,1,1);
    begin;
    insert into t values(null,2,2);
    rollback;
    insert into t values(null,2,2);
    
    • 自增值为什么不能回退。

    假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增id,肯定要加锁,然后顺序申请。

    1. 假设事务A申请到了id=2, 事务B申请到id=3,那么这时候表t的自增值是4,之后继续执行。
    2. 事务B正确提交了,但事务A出现了唯一键冲突。
    3. 如果允许事务A把自增id回退,也就是把表t的当前自增值改回2,那么就会出现这样的情况:表里面已经有id=3的行,而当前的自增id值是2。
    4. 接下来,继续执行的其他事务就会申请到id=2,然后再申请到id=3。这时,就会出现插入语句报错“主键冲突”。

    为了解决这个主键冲突有两种方法:

    1. 每次申请id之前,先判断表里面是否已经存在这个id。如果存在,就跳过这个id。但是,这个方法的成本很高。因为,本来申请id是一个很快的操作,现在还要再去主键索引树上判断id是否存在。
    2. 把自增id的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。

    因此,InnoDB放弃了这个设计,语句执行失败也不回退自增id。也正是因为这样,所以才只保证了自增id是递增的,但不保证是连续的。

    小结:

    1. 在mysql 8.0 版本,将自增值的变更记录增加到redo log中,所以做到了自增值的持久化,之前的版本中都会找自增值的最大maxid+1作为自增值。
    2. auto_increment_offset 和 auto_increment_increment是两个系统参数,分别用来表示自增的初始值和步长,默认值都是1。注意和自增值概念的区分
    3. 主键冲突或者事物回滚导致自增值虽然增加了,但是插入数据失败,是自增值不连续的主要原因。
  • 相关阅读:
    DLL编写中extern “C”和__stdcall的作用
    spring mvc controller中的异常封装
    springMVC 【@response 返回对象自动变成json并且防止乱码】 & 【配置支持实体类中的@DateTimeFormat注解】
    Eclipse中修改SVN用户名和密码方法
    Maven
    j2ee、mvn、eclipse、Tomcat等中文乱码问题解决方法
    maven生成jar包
    windows超过最大连接数解决命令
    spring 国际化-i18n
    springMVC 前后台日期格式传值解决方式之二(共二) @InitBinder的使用
  • 原文地址:https://www.cnblogs.com/jimmyhe/p/11216743.html
Copyright © 2020-2023  润新知