MySQL 常见问题总结
MySQL为什么会抖一下?
-
针对InnoDB导致MySQL抖得原因,主要是InnoDB会在后台刷脏页,而刷脏页的过程是要将内存页写入磁盘。
所以,无论是你的查询语句在需要内存的时候可能要求淘汰一个脏页,还是由于刷脏页的逻辑会占用IO资源并可能影响到了你要更新的语句,都可能造成MySQL"抖"一下原因;
怎么解决MySQL抖动的问题?
- 设置合理参数配置,
innodb_io_capacity
的值,并且平时要多关注脏页比例,不要让它经常接近75%;
什么是脏页?
- 当内存数据页跟磁盘数据页内容不一致的时候,我们称为这个内存页为"脏页";
什么是干净页?
-
内存数据写入到磁盘后,内存和磁盘上的数据页内容就达到一致,称为"干净页";
不论是脏页还是干净页,都在内存中;
脏页是怎么产生的?
- 因为使用了WAL(write-ahead-log先写入日志,在写入磁盘)技术,这个技术会把数据库的随机写转化为顺序写,会产生脏页;
什么是随机写?为什么耗性能?
- 随机写,需要重新定位位置,机械运动是很慢的,即使不是机械运动重新定位写磁盘的位置也是很耗时的;
什么是顺序写?
- 数据写磁盘上的扇区就在上次的下一个位置,不需要重新定位写磁盘的位置速度会比随机写快一些;
WAL怎么把随机写转换为顺序写的?
- 因为写redo log是顺序写的,先写redo log 等合适时机在写入磁盘,间接的将随机写变成顺序写,性能确实会提高不少;
为什么删除表的数据,表文件大小没发生改变?
- 因为delete命令其实只是把记录的位置,或者数据页标记为"逻辑删除",但磁盘文件的大小是不会变的,物理空间没有实际释放;
表的数据信息存在哪里?
-
它可以存储在共享表空间里,也可以单独存储在文件以
.ibd
为后缀的文件里。由参数innodb_file_per_table
来控制。 -
innodb_file_per_table:参数OFF表示,共享空间;参数ON表示,以.ibd后缀文件;
建议存储在单独的文件,因为在不需要的时候,使用drop table命令也能直接把对应的文件删除,如果存储在共享空间之中,即使表删除了空间也不会释放;
MySQL5.6.6版本开始,它的默认值就是ON;
表的结构信息存在哪里?
-
表的结构定义占有存储空间比较小,在MySQL8.0之前,表结构的定义信息存在
.frm
后缀的文件里;在MySQL8.0之后,允许把表结构的定义信息存在系统数据表之中;系统数据表,主要用于存储MySQL系统数据,比如:数据字典、undo log(默认)等文件
如何删除表数据后,表文件资源释放?
重建表,消除表因为进行大量的增删改操作而产生空洞,使用如下命令:
1:alter table t engine=innodb;
2:optimize table t(等于 recreate + analyze);
3:truncate table t(等于 drop + create);
analyze table t 其实不是重建表,只是对表的索引信息重新统计,没有修改数据,这个过程中加了MDL读锁;
什么是空洞?如何产生?
- 空洞就是那些被标记可复用,但是还没被使用的存储空间;
- 使用delete命令删除数据会产生空洞,标记为可复用;
- 插入新的数据可能引起页分裂,也可能产生空洞;
- 修改操作,有时是一种先删除后新增的动作也可能产生空洞;
什么是count()语句?
不同的存储引擎实现方式一样:
-
MyISAM引擎把一个表的总行数存在磁盘上,因此执行count(*)的时候会直接返回这个数,效率很高;
-
InnoDB引擎它执行count(*)的时候,需要把数据一行一行地从引擎里面读出来,然后在累积计数;
对于InnoDB引擎来说,count()是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是NULL,累计值+1,否则不加,最后返回累计值;
count()效率排序、如何计数?
-
count(字段) < count(主键ID) < count(1) ≈ count(*)
尽量使用count(*)
-
count(字段)计数:
- 若"字段" 定义为 not null,一行行地从记录里面读出这个字段,判断不能为null,按行累加;
- 若"字段"定义允许为 null,那么执行的时,判断到有可能是null,还要把值取出来在判断一下,不是null在累加;
-
count(主键ID)计数:InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断不可能为空,就按行累加。从引擎返回的主键ID会涉及到解析数据行,以及拷贝字段值的操作;
-
count(1):InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字"1"进去,判断不可能为空,按行累加;
-
count(*):并不会把全部字段取出来,而是专门做了优化,不取值。count( *)肯定不是null按行累加;
什么是幻读?
-
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
幻读在"当前读"下会出现;幻读仅专指"新插入的行";
for update 语句:表示当前读; 当前读的规则,就是要能读到所有已经提交的记录的最新值;
update 的加锁语义和select…for update 是一致的;
如何解决幻读?
-
间隙锁(Gap lock):两个值之间的锁;
间隙锁和行锁合称:next-key lock:每个next-key lock是前开后闭区间;
间隙锁引入什么问题?
-
可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的;
-
间隙锁在RR级别下才有效,RC级别下无间隙锁;
不使用间隙锁方法:
使用RC隔离 + binlog_format = row 组合;
为什么有kill不掉的语句?
-
这些的"kill 不掉的情况",是因为发送kill命令的客户端,并没有强行停止目标线程的执行,而只是设置了状态,并唤醒对应的线程;而被kill的线程,需要执行到判断的状态"埋点",才会开始进入终止逻辑阶段(耗费时间);
-
如果你发现一个线程处于killed状态,可做的事情,通过影响系统环境,让这个killed状态尽快结束;
比如InnoDB并发的问题,就可以临时调大innodb_thread_concurrency的值,或者停掉别的线程,让出位子给这个线程执行;
而回滚逻辑由于收到IO资源限制执行得比较慢,就通过减少系统压力让它加速;
-
kill query + 线程ID:表示终止这个线程中正在执行的语句;
-
kill connection + 线程ID:断开这个线程的(show processlist 会显示killed);
查大量数据,会不会把数据库内存打爆?
-
由于MySQL采用的是边算边发的逻辑,因此对于数据量很大的查询结果来说,不会在server端保存完整的结果集。所以,客户端结果不及时,会堵住MySQL查询过程,但不会把内存打爆。
如果一个查询的返回结果不会很多的话,建议使用my_sql_store_result,这个接口,直接把查询结果保存到本地内存
show processlist;
当一个线程处于,等待客户端接收结果的状态,会显示"Sending to client",如果显示成"Sending data",它的意思只是正在执行;
-
对于InnoDB引擎内部,由于有淘汰策略,大查询也不会导致内存暴涨。并且,由于InnoDB对LRU算法做了改进,冷数据的全表扫描,对Buffer Pool的影响也能做到可控。
Buffer Pool 有加速查询的作用,依赖于一个重要的指标,“内存命中率”,使用命令show engine innodb status,查看一个系统当前的BP命中率,一般情况下,一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率在99%以上;
InnoDB Buffer Pool 的大小由参数innodb_buffer_pool_size确定,一般建议设置成可用物理内存60%~80%,如果一个Buffer Pool满了,而要从磁盘读入一个数据页,那肯定是要淘汰一个旧的数据页;
InnoDB内存管理用的是(Least Recently Used,LRU)算法,这个算法的核心就是淘汰最久未使用的数据;
什么场景自增主键可能不是连续的?
1.唯一键冲突;
2.事务回滚;
3.自增主键的批量申请;
自增主键的作用?
- 让主键索引尽量地保持递增顺序插入,避免页分裂,使索引更紧凑;
自增主键的保存机制?
不同的存储引擎,机制不同:
- MyISAM引擎的自增值保存在数据文件中;
- InnoDB引擎的自增值,保存在了内存里,到了MySQL 8.0版本后,才有了"自增值持久化"的能力,放在了red log里面;
自增主键的修改机制?
在MySQL里面,如果字段id被定义为AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:
1.如果插入数据时,id字段指定为0、null或者未指定值,那么把这个表当前的AUTO_INCREMENT值填到自增字段;
2.如果插入数据时id字段,指定了具体的值,就直接使用语句里指定的值;
插入值跟当前自增值的关系:
根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是X,当前的自增值是Y。
1:如果 X < Y,那么这个表的自增值不变;
2:如果 X ≥ Y,那就需要把当前自增值修改为新的自增值;
两表之间拷贝数据用什么方法,有什么注意事项?
1.使用insert…select 两表之间拷贝数据:
需要注意,可重复读隔离级别下,这个语句会给select的表里扫描到的记录和间隙加读锁;
如果insert…select 的对象是同一个表,有可能会造成循环写入。这种情况下,我们需要引入用户临时表来做优化;
insert 语句如果出现唯一键冲突,会在冲突的唯一值上加共享的next-key lock(S锁)。如果碰到由于唯一键约束导致报错后,要尽快提交或回滚事务,避免加锁时间过长;
2.导出成execl,然后拼sql成insert into values(),(),()形成,进行批量插入再;
3.多个线程分到几个任务执行,比如十个线程,每个线程10条记录,插入后,再查询新的100条记录处理;
怎么最快地复制一张表?
1.物理拷贝的方式速度最快,尤其是对大表拷贝来说是最快的方法;如果出现误删表的情况,用备份恢复出,误删之前的临时库,然后再把临时库中的表拷贝到生产库上,是恢复数据最快的方法。但是也会有一定的局限性:
- 必须是全表拷贝,不能拷贝部分数据;
- 需要到服务器上拷贝数据,再用户无法登录数据库主机的场景下无法使用;
- 由于通过拷贝物理文件实现的,源表和目标表都是使用InnoDB引擎时才能使用;
2.使用mysqldump生成包含INSERT语句文件的方法,可以在where参数增加过滤条件,来实现只导出部分数据。这个方式不足之处,不能使用join这种比较复杂的where条件写法;
3.用select … into outfile 的方法是最灵活的,支持所有的SQL写法。但这个方法的缺点就是,每次只能导出一张表的数据,而且表结构也需要另外的语句单独备份;
后两种方式都是逻辑备份方式,是可以跨引擎使用的。
物理拷贝表的功能:
查看当前mysql数据文件所存放的位置:
mysql> show global variables like "%datadir%";
+---------------+---------------------------------------------+
| Variable_name | Value |
+---------------+---------------------------------------------+
| datadir | C:\ProgramData\MySQL\MySQL Server 5.7\Data\ |
+---------------+---------------------------------------------+
在MySQL5.6版本引入了可传输表空间的方法,可以通过导出 + 导入表空间的方式,实现物理拷贝表的功能;
举个栗子:
假设目标在db1库下,复制一个跟表t相同的表r,具体执行步骤如下:
执行create table r like t; 创建一个相同表结构的空表;
执行alter table r discard tablespace; 这时候r.ibd文件会被删除;
执行flush table t for export; 这时候db1目录下会生成一个t.cfg文件;
在db1目录下执行 cp t.cfg r.cfg; cp t.ibd r.ibd; 这两个命令(拷贝得到两个文件, MYSQL进行要有读写权限);
执行unlock tables; 这时候t.cfg 文件会被删除;
执行alter table r import tablespace; 将这个r.ibd 文件作为表r的新的表空间, 由于文件的数据内容和t.ibd是相同的,所以表r中就有了和表t相同的数据;
-
需要注意点:
在第3步执行完flshu table 命令后,db1.t整个表处于只读状态,执行unlock tables 命令后才释放读锁;
在执行import tablespace的时候,让文件里的表空间id和数据字典中一致,会修改r.ibd表的空间id。而表空间id存在每一个数据页中。如果文件很大,每个数据页都需要修改,可能import 语句执行是需要一些时间的。
MySQL中索引结构和Buffer Pool相关知识点:
1.在对被驱动表做全表扫描的时候,如果数据没有在Buffer Pool中,就需要等待这部分数据从磁盘读入;
从磁盘读入数据到内存中,会影响正常业务的Buffer Pool命中率,而且这个算法天然会对被驱动表的数据做多次访问,更容易将这些数据页放到Buffer Pool的头部;
2.即使被驱动表数据都在内存中,每次查找"下一个记录的操作",都是类似指针操作。而join_buffer中是数组,遍历的成本更低;
BNL算法的性能会更好;