ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

临时表、内存表、自增主键疑难杂症

2021-04-13 20:02:12  阅读:206  来源: 互联网

标签:语句 自增 增值 疑难杂症 id 插入 session 主键


文章目录


临时表和内存表,这两个概念是完全不同的。

一、临时表

1.0、区别

  • 内存表,指的是使用 Memory 引擎的表,建表语法是 create table …
    engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表
    结构还在。除了这两个特性看上去比较“奇怪”外,从其他的特征上看,它就是一个正常
    的表。
  • 而临时表,可以使用各种引擎类型 。如果是使用 InnoDB 引擎或者 MyISAM 引擎的临
    时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用 Memory 引擎

1.1、临时表的特性

在这里插入图片描述
临时表的特点:

  1. 建表语法是 create temporary table …。
  2. 一个临时表只能被创建它的 session 访问,对其他线程不可见。所以,图中 session A
    创建的临时表 t,对于 session B 就是不可见的。
  3. 临时表可以与普通表同名。
  4. session A 内有同名的临时表和普通表的时候,show create 语句,以及增删改查语句访
    问的是临时表。
  5. show tables 命令不显示临时表。

由于临时表只能被创建它的 session 访问,所以在这个 session 结束的时候,会自动删除临时表。也正是由于这个特性,临时表就特别适合我们文章开头的 join 优化这种场景。为什么呢?

原因主要包括以下两个方面:

  1. 不同 session 的临时表是可以重名的,如果有多个 session 同时执行 join 优化,不需要
    担心表名重复导致建表失败的问题。
  2. 不需要担心数据删除问题。如果使用普通表,在流程执行过程中客户端发生了异常断
    开,或者数据库发生异常重启,还需要专门来清理中间过程中生成的数据表。而临时表
    由于会自动回收,所以不需要这个额外的操作。

1.2、临时表的应用(临时表名重复)

由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。
一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大表 ht,按照字段 f,拆分成 1024 个分表,然后分布到 32 个数据库实例上。如下图所示:在这里插入图片描述
一般情况下,这种分库分表系统都有一个中间层 proxy。不过,也有一些方案会让客户端直接连接数据库,也就是没有 proxy 这一层。
在这个架构中,分区 key 的选择是以“减少跨库和跨表查询”为依据的。如果大部分的语句都会包含 f 的等值条件,那么就要用 f 做分区键。这样,在 proxy 这一层解析完 SQL 语句以后,就能确定将这条语句路由到哪个分表做查询。

1 select v from ht where f=N;

这时,我们就可以通过分表规则(比如,N%1024) 来确认需要的数据被放在了哪个分表上。这种语句只需要访问一个分表,是分库分表方案最欢迎的语句形式了。
但是,如果这个表上还有另外一个索引 k,并且查询语句是这样的:

1 select v from ht where k >= M order by t_modified desc limit 100;

这时候,由于查询条件里面没有用到分区字段 f,只能到所有的分区中去查找满足条件的所有行,然后统一做 order by 的操作。这种情况下,有两种比较常用的思路。

  • 第一种思路是,在 proxy 层的进程代码中实现排序。
    这种方式的优势是处理速度快,拿到分库的数据以后,直接在内存中参与计算。不过,这个
    方案的缺点也比较明显:
  1. 需要的开发工作量比较大。我们举例的这条语句还算是比较简单的,如果涉及到复杂的
    操作,比如 group by,甚至 join 这样的操作,对中间层的开发能力要求比较高;
  2. 对 proxy 端的压力比较大,尤其是很容易出现内存不够用和 CPU 瓶颈的问题。
  • 另一种思路就是,把各个分库拿到的数据,汇总到一个 MySQL 实例的一个表中,然后在
    这个汇总实例上做逻辑操作。
    比如上面这条语句,执行流程可以类似这样:
    1 select v from ht where k >= M order by t_modified desc limit 100;
    在汇总库上创建一个临时表 temp_ht,表里包含三个字段 v、k、t_modified;
    在各个分库上执行
1 select v,k,t_modified from ht_x where k >= M order by t_modified desc limit 100;

把分库执行的结果插入到 temp_ht 表中;
执行

1 select v from temp_ht order by t_modified desc limit 100; 

在这里插入图片描述
在实践中,我们往往会发现每个分库的计算量都不饱和,所以会直接把临时表 temp_ht 放到 32 个分库中的某一个上。
在这里插入图片描述

1.3、主备中如何使用临时表(主库上不同的线程创建同名的临时表是没关系的,但是传到备库执行是怎么处理的呢?)

在这里插入图片描述

在这里插入图片描述

1.4、为什么不能用 rename 修改临时表的改名。

在实现上,执行 rename table 语句的时候,要求按照“库名 / 表名.frm”的规则去磁盘找文件,但是临时表在磁盘上的 frm 文件是放在 tmpdir 目录下的,并且文件名的规则是“#sql{进程 id}{线程 id} 序列号.frm”,因此会报“找不到文件名”的错误。

1.5、group by的使用

  1. 如果对 group by 语句的结果没有排序要求,要在语句后面加 order by null;
  2. 尽量让 group by 过程用上表的索引,确认方法是 explain 结果里没有 Using
    temporary 和 Using filesort;
  3. 如果 group by 需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大
    tmp_table_size 参数,来避免用到磁盘临时表;
  4. 如果数据量实在太大,使用 SQL_BIG_RESULT 这个提示,来告诉优化器直接使用排序算法得到 group by 的结果。

二、内存表

由于重启会丢数据,如果一个备库重启,会导致主备同步线程停止;如果主库跟
这个备库是双 M 架构,还可能导致主库的内存表数据被删掉。在生产上,我不建议你使用普通内存表。
如果你是 DBA,可以在建表的审核系统中增加这类规则,要求业务改用 InnoDB 表。我们在文中也分析了,其实 InnoDB 表性能还不错,而且数据安全也有保障。而内存表由于不支持行锁,更新语句会阻塞查询,性能也未必就如想象中那么好。
基于内存表的特性,我们还分析了它的一个适用场景,就是内存临时表。内存表支持 hash索引,这个特性利用起来,对复杂查询的加速效果还是很不错的。

2.1、如果你维护的 MySQL 系统里有内存表,怎么避免内存表突然丢数据,然后导致主备同步停止的情况。

我们假设的是主库暂时不能修改引擎,那么就把备库的内存表引擎先都改成 InnoDB。对于
每个内存表,执行

set sql_log_bin=off;
alter table tbl_name engine=innodb;

这样就能避免备库重启的时候,数据丢失的问题。
由于主库重启后,会往 binlog 里面写“delete from tbl_name”,这个命令传到备库,备库的同名的表数据也会被清空。
因此,就不会出现主备同步停止的问题。
如果由于主库异常重启,触发了 HA,这时候我们之前修改过引擎的备库变成了主库。而原
来的主库变成了新备库,在新备库上把所有的内存表(这时候表里没数据)都改成 InnoDB表。
所以,如果我们不能直接修改主库上的表引擎,可以配置一个自动巡检的工具,在备库上发
现内存表就把引擎改了。同时,跟业务开发同学约定好建表规则,避免创建新的内存表。

2.2、将数据保存到 InnoDB 表用来持久化

三、为什么自增主键不连续

3.1、自增值保存在哪儿?

在这个空表 t 里面执行 insert into t values(null, 1, 1); 插入一行数据,再执行 show
create table 命令,就可以看到如下图所示的结果:
在这里插入图片描述
可以看到,表定义里面出现了一个 AUTO_INCREMENT=2,表示下一次插入数据时,如果需要自动生成自增值,会生成 id=2。
其实,这个输出结果容易引起这样的误解:自增值是保存在表结构定义里的。实际上,表的结构定义存放在后缀名为.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 恢复重启之前的值。

3.2、自增值修改机制

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

  1. 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的
    AUTO_INCREMENT 值填到自增字段;
  2. 如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。
    根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是 X,当前的自增值是 Y。
  3. 如果 X<Y,那么这个表的自增值不变;
  4. 如果 X≥Y,就需要把当前自增值修改为新的自增值。

3.3、新的自增值生成算法

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

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

  1. 如果准备插入的值 >= 当前自增值,新的自增值就是“准备插入的值 +1”;
  2. 否则,自增值不变。
    这就引入了我们文章开头提到的问题,在这两个参数都设置为 1 的时候,自增主键 id 却不能保证是连续的,这是什么原因呢?

3.4、自增值的修改时机

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. 继续执行插入数据操作,由于已经存在 c=1 的记录,所以报 Duplicate key error,语
    句返回。
    对应的执行流程图如下:
    在这里插入图片描述
    可以看到,这个表的自增值改成 3,是在真正执行插入数据的操作之前。这个语句真正执行的时候,因为碰到唯一键 c 冲突,所以 id=2 这一行并没有插入成功,但也没有将自增值再改回去。
    所以,在这之后,再插入新的数据行时,拿到的自增 id 就是 3。也就是说,出现了自增主
    键不连续的情况。
    在这里插入图片描述
    可以看到,这个操作序列复现了一个自增主键 id 不连续的现场 (没有 id=2 的行)。可见,
    唯一键冲突是导致自增主键 id 不连续的第一种原因。
    同样地,事务回滚也会产生类似的现象,这就是第二种原因。

3.5、自增值为什么不能回退

假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增
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。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。
    可见,这两个方法都会导致性能问题。造成这些麻烦的罪魁祸首,就是我们假设的这个“允许自增 id 回退”的前提导致的。
    因此,InnoDB 放弃了这个设计,语句执行失败也不回退自增 id。也正是因为这样,所以
    才只保证了自增 id 是递增的,但不保证是连续的。

3.6、自增锁的优化

自增 id 锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务
再申请。其实,在 MySQL 5.1 版本之前,并不是这样的。
接下来,我会先给你介绍下自增锁设计的历史,这样有助于你分析接下来的一个问题。
在 MySQL 5.0 版本的时候,自增锁的范围是语句级别。也就是说,如果一个语句申请了一个表自增锁,这个锁会等语句执行结束以后才释放。显然,这样设计会影响并发度。
MySQL 5.1.22 版本引入了一个新策略,新增参数 innodb_autoinc_lock_mode,默认值是 1。

  1. 这个参数的值被设置为 0 时,表示采用之前 MySQL 5.0 版本的策略,即语句执行结束
    后才释放锁;
  2. 这个参数的值被设置为 1 时
    普通 insert 语句,自增锁在申请之后就马上释放;
    类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释
    放;
  3. 这个参数的值被设置为 2 时,所有的申请自增主键的动作都是申请后就释放锁。

3.7、为什么默认设置下,insert … select 要使用语句级的锁?为什么这个参数的默认值不是 2?

答案是,这么设计还是为了数据的一致性。
在这里插入图片描述
在这个例子里,我往表 t1 中插入了 4 行数据,然后创建了一个相同结构的表 t2,然后两个
session 同时执行向表 t2 中插入数据的操作。
你可以设想一下,如果 session B 是申请了自增值以后马上就释放自增锁,那么就可能出现
这样的情况:

session B 先插入了两个记录,(1,1,1)、(2,2,2);
然后,session A 来申请自增 id 得到 id=3,插入了(3,5,5);
之后,session B 继续执行,插入两条记录 (4,3,3)、 (5,4,4)

你可能会说,这也没关系吧,毕竟 session B 的语义本身就没有要求表 t2 的所有行的数据都跟 session A 相同。
是的,从数据逻辑上看是对的。但是,如果我们现在的 binlog_format=statement,你可以设想下,binlog 会怎么记录呢?
由于两个 session 是同时执行插入数据命令的,所以 binlog 里面对表 t2 的更新日志只有两种情况:要么先记 session A 的,要么先记 session B 的。

你可能会说,这也没关系吧,毕竟 session B 的语义本身就没有要求表 t2 的所有行的数据都跟 session A 相同。
是的,从数据逻辑上看是对的。但是,如果我们现在的 binlog_format=statement,你可以设想下,binlog 会怎么记录呢?
由于两个 session 是同时执行插入数据命令的,所以 binlog 里面对表 t2 的更新日志只有两种情况:要么先记 session A 的,要么先记 session B 的。

其实,这是因为原库 session B 的 insert 语句,生成的 id 不连续。这个不连续的 id,用statement 格式的 binlog 来串行执行,是执行不出来的。
而要解决这个问题,有两种思路:

  1. 一种思路是,让原库的批量插入数据语句,固定生成连续的 id 值。所以,自增锁直到语
    句执行结束才释放,就是为了达到这个目的。
  2. 另一种思路是,在 binlog 里面把插入数据的操作都如实记录进来,到备库执行的时候,
    不再依赖于自增主键去生成。这种情况,其实就是 innodb_autoinc_lock_mode 设置为
    2,同时 binlog_format 设置为 row。

因此,在生产上,尤其是有 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 的策略:

  1. 语句执行过程中,第一次申请自增 id,会分配 1 个;
  2. 1 个用完以后,这个语句第二次申请自增 id,会分配 2 个;
  3. 2 个用完以后,还是这个语句,第三次申请自增 id,会分配 4 个;
  4. 依此类推,同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍。
insert…select,实际上往表 t2 中插入了 4 行数据。但是,这四行数据是分三次申请的自增
id,第一次申请到了 id=1,第二次被分配了 id=2 和 id=3, 第三次被分配到 id=4 到
id=7。
由于这条语句实际只用上了 4 个 id,所以 id=5 到 id=7 就被浪费掉了。之后,再执行
insert into t2 values(null, 5,5),实际上插入的数据就是(8,5,5)。
这是主键 id 出现自增 id 不连续的第三种原因。

insert…select,实际上往表 t2 中插入了 4 行数据。但是,这四行数据是分三次申请的自增
id,第一次申请到了 id=1,第二次被分配了 id=2 和 id=3, 第三次被分配到 id=4 到
id=7。
由于这条语句实际只用上了 4 个 id,所以 id=5 到 id=7 就被浪费掉了。之后,再执行
insert into t2 values(null, 5,5),实际上插入的数据就是(8,5,5)。
这是主键 id 出现自增 id 不连续的第三种原因。

标签:语句,自增,增值,疑难杂症,id,插入,session,主键
来源: https://blog.csdn.net/wolf_love666/article/details/115676035

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有