关于极客时间 | MySQL实战45讲的部分总结

文章目录

  • MySQL为什么有时候会选错索引
    • 1.MySQL选择索引的依据
      • 1.1 基于主键的成本计算
      • 1.2 对于二级索引+回表方式的成本计算
    • 2.基于索引统计数据的成本计算
      • 2.1 index dive
      • 2.2 Cardinality(基数)属性
      • 2.3 对于超过dive的系统变量的情况
        • 2.3.1 MySQL选错索引后的优化
  • 怎么给字符串字段加索引
    • 1.加索引的两种思路
      • 1.1 不指定长度的索引
      • 1.2 指定长度的前缀索引
      • 1.3 存在的问题
  • 为什么表数据删掉一半,表文件大小不变?
    • 1.空洞
      • 1.1 如何处理空洞
      • 1.2 Online和inplace
      • 1.3 什么情况下会造成重建完表还变大的情况
  • count(*)这么慢,我该怎么办?
    • 1.count(*) 的实现方式
    • 2.为什么InnoDB也不把总行数存起来呢
    • 3.InnoDB对count(*)的优化
    • 4.所以怎么优化才能提高效率呢
      • 4.1 用缓存系统保存计数
      • 4.2 在数据库保存计数
    • 5.基于InnoDB不同的count用法效率
  • order by 是怎么工作的?
    • 使用order by产生排序的情况
      • 1.全字段排序
        • 1.1 完全在内存中完成
        • 1.2 借助磁盘进行排序
      • 2.rowid排序
      • 3.两种排序的对比
    • 2.使用order by但没有进行排序的情况
  • 如何正确的使用随机排序
    • 1.order by rand()
    • 2.磁盘临时表
    • 3.随机排序算法
  • 为什么sql逻辑一样但执行效率却不同
    • 1.前言
    • 2.隐式类型转换
    • 3.隐式字符编码转换
  • 查一行也会很慢?
    • 1.查询长时间不返回
    • 等MDL锁
    • 等flush
    • 等行锁
    • 2.查询慢
  • 幻读问题究极解决方法
    • 1.读操作利用MVCC,写操作加锁
    • 2.读写操作都进行加锁
      • 2.1 语义上的问题
      • 2.2 数据一致性的问题
    • 3.间隙锁(Gap lock)
      • 3.1 间隙锁引入所带来的问题
        • 3.1.1 死锁问题
  • 为什么改一条记录这么多锁?
    • 1.加锁原则
    • 2.根据加锁原则得到的优化
  • MySQL有哪些“饮鸩止渴”提高性能的方法?
    • 1.先处理掉那些占着连接但是不工作的线程。
    • 2.减少连接过程的消耗。
    • 3.慢查询导致的性能问题及解决方法
      • 3.1 索引没有设计好
      • 3.2 SQL 语句没写好
      • 3.3 mysql自己选错了索引
      • 3.4 总结
    • 4.QPS激增问题
    • 5.类似饮鸩止渴的场景
  • binlog
    • WAL
    • 1.binlog写入机制
    • 2.配合redo log的参数设置以优化IO带来的性能瓶颈问题
      • 2.1 优化参考
  • MYSQL是如何保证主备一致的?
    • 1.主备一致的基本原理
    • 2.binlog
      • 2.1 statement格式的binlog
      • 2.2 row格式的binlog
      • 2.3 mixed格式的binlog
      • 2.4 设置推荐
    • 3.循环复制的问题
      • 3.1 循环问题概述
  • MYSQL如何保证高可用
    • 0.MYSQL主从复制(集群)的常见解决方案
    • 1.MYSQL导致主备延迟的原因
      • 1.1 数据同步的关键时间点
      • 1.2 原因
    • 2.优化策略
      • 2.1 可靠性优先策略
      • 2.2 可用性优先策略
      • 2.3 总结
    • 3.备机的并行复制能力演变
      • 3.1 引入mysql复制的两种复制策略
      • 3.2 MySQL 5.6 版本的并行复制策略
      • 3.3 MariaDB 的并行复制策略
      • 3.4 MySQL 5.7 的并行复制策略
      • 3.5 MySQL 5.7.22 的并行复制策略
  • 主库出问题了,从库怎么办
    • 1.主库宕机后的主备切换
      • 1.1 切换流程
      • 1.2 异常解决的替代
    • 2.读写分离的坑
  • 误删数据后除了跑路,还能怎么办?
  • 为什么还有kill不掉的语句?
    • 1.kill的过程
    • 2.出现killed(杀不掉)的两种情况
    • 3.两个客户端的误解
  • 我查这么多数据,会不会把数据库内存打爆?
    • 1.客户端和服务端如何进行查询
  • join语句怎么优化
    • 0.基于回表的优化
    • 1.循环嵌套连接(NLJ)
    • 2.基于块的循环嵌套连接(BNL)
    • 3.hash存储
  • 临时表
    • 1.临时表的特性
    • 2.临时表的应用
    • 3.为什么临时表可以重名
    • 4.临时表和主备复制
    • 4.内部临时表
      • 4.1 什么使用能使用到内部临时表
      • 4.2 总结
  • Memory引擎
    • 1.内存表的数据组织结构
    • 2.hash 索引和 B-Tree 索引
    • 3.为什么线上不推荐你使用Memory
      • 3.1 锁粒度问题
      • 3.2 数据持久化问题
      • 3.3 总结
  • 自增键为什么不是连续的
    • 1.自增键存储在哪里
    • 2.自增值修改机制
      • 3.为什么自增值不连续
        • 3.1 唯一键冲突导致自增值不连续(第一种原因)
        • 3.2 事务回滚导致不连续(第二种原因)
        • 3.3 自增值为什么不能回退
      • 4.自增锁的优化(第三种原因,主要就是自增id申请策略)
  • 不是普通的insert锁为什么这么多
    • 1.insert … select 语句
    • 2.insert循环写入
    • 3.insert唯一键冲突
    • 4.insert into … on duplicate key update
  • 如何快速的复制一张表
    • 1.mysqldump方法
    • 2.导出CSV文件
  • grant之后要跟着flush privileges吗?
    • 1.全局权限
    • 2.库(db)权限
    • 3.表权限和列权限
    • 4.使用flush privileges之前的总结
    • 5.flush privileges 使用场景
  • 要不要使用分区表
    • 1.分区表是什么
      • 1.1 InnoDB分区表的引擎层的行为
      • 1.2 引擎是MyISAM会发生什么
    • 2.手动分表与分区表的区别
      • 2.1 分区策略
      • 2.2 分区表的 server 层行为
    • 3.小结
      • 3.1 分区表的应用场景
  • 关于join的补充、NLJ和BNL的补充、group by和distinct的补充
  • 自增ID用完了怎么办

MySQL为什么有时候会选错索引

1.MySQL选择索引的依据

  • 概述

mysql选择索引无非就是优化器觉得哪一种索引的成本执行低,优化器会从该索引的扫描行数、是否会用到临时表及是否要进行排序来进行综合判断

1.1 基于主键的成本计算

  • 概述

对于主键来说就是计算全表扫描的成本,取决于下面两个方面

  • 聚簇索引占用的页面数
  • 该表中的记录数
  • 该表中的记录数

对于这个数据的获取,mysql使用的是统计数据中的估计值,每个表都会有一些列的统计信息,本选项表示表中的记录条数。对于使用 MyISAM 存储引擎的表来说,该值是准确的,对于使用 InnoDB 存储引擎的表来说,该值是一个估计值。SHOW TABLE STATUS 展示出的 Rows 值

1.2 对于二级索引+回表方式的成本计算

  • 概述

对于这个方式主要取决于

  • 范围区间数量
  • 需要回表的数量:这个会寻找需要回表记录中第一满足这个条件的记录和最后一个满足这个条件的记录(例如一个范围0~100,会找第一个大于0的最左边界,和找第一个满足小于100的最右边界),这两个记录如果位于不同的页面(对于二级索引列在B+树中是连续的),那么就找父结点来计算总共有几页

2.基于索引统计数据的成本计算

2.1 index dive

  • 概述

前面说到的根据左右区别计算回表的数量(记录条数少的时候可以做到精确计算,多的时候只能估算)。 MySQL把这种通过直接访问索引对应的 B+ 树来计算某个范围区间对应的索引记录条数的方式称之为 index dive 。直接利用索引对应的B+树来计算某个范围区间对应的记录条数

  • 局限性

有零星几个单点区间的话,使用 index dive 的方式去计算这些单点区间对应的记录数也不是什么问题,可是你架不住有的孩子憋足了劲往 IN 语句里塞东西呀,我就见过有的同学写的 IN 语句里有20000个参数的,这就意味着 MySQL 的查询优化器为了计算这些单点区间对应的索引记录条数,要进行20000次 index dive 操作,这性能损耗可就大了,搞不好计算这些单点区间对应的索引记录条数的成本比直接全表扫描的成本都大了

  • 解决

mysql提供了一个系统变量eq_range_index_dive_limit,默认值为200,如果需要做的dive量超过这个,就要使用统计数据来进行估算

2.2 Cardinality(基数)属性

  • 概述

该属性是存储索引统计数据中的某个属性,你可以通过 SHOW INDEX FROM 表名 的语法来查看某个表的各个索引的统计数据,这个属性值表达索引列中不重复值的数量,对于InnoDB存储引擎来说,使用SHOW INDEX语句展示出来的某个索引列的**Cardinality属性是一个估计值,并不是精确的**

  • 统计方式

采样统计的时候,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。

2.3 对于超过dive的系统变量的情况

  • 概述

此时就要用到两个属性来估算此次的成本

  • 使用 SHOW TABLE STATUS 展示出的 Rows 值,也就是一个表中有多少条记录。
  • 使用 SHOW INDEX 语句展示出的 Cardinality 属性。
2.3.1 MySQL选错索引后的优化
  • 概述

当上面两个属性统计出来及其不准确,mysql就会放弃使用当前索引,转为优化器自身认为的最优,当然宏观上我们看来并不是最优

  1. 对于统计信息非常不准确(例如并发另外一个线程删除后又重新插入这种类似的就会导致统计信息的错误,因为删除只是把delete mark改一下,并没有真正的删除)我们可以使用analyze table来矫正,实在不行在用下面的
  2. 使用force index指定索引:在用在生产环境上灵活性太差
  3. 我们可以考虑修改语句,引导 MySQL 使用我们期望的索引(例如limit语句的限制)
  4. 在有些场景下,我们可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引

怎么给字符串字段加索引

  • 概述

当今越来越多的用户名可以使用邮箱来创建,那么给邮箱等比较复杂的字符串创建索引也成了刚需,那么如何给字符串加索引能提高效率呢

1.加索引的两种思路

1.1 不指定长度的索引

  • 概述

当你使用下列语句创建时就会将字符串全部包含,当进行查询时步骤如下,也就是只回表了一次

  • 从 index1 索引树找到满足索引值是’[email protected]’的这条记录,取得 ID2 的值;
  • 到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集;
  • 取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='[email protected]’的条件了,循环结束。
alter table SUser add index index1(email);
# 查询
select id,name,email from SUser where email='[email protected]';

1.2 指定长度的前缀索引

  • 概述

当你使用下列指定字符串长度建立索引时,查询过程如下,可以看到回了四次表

  • 从 index2 索引树找到满足索引值是’zhangs’的记录,找到的第一个是 ID1;
  • 到主键上查到主键值是 ID1 的行,判断出 email 的值不是’[email protected]’,这行记录丢弃;
  • 取 index2 上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出 ID2,再到 ID 索引上取整行然后判断,这次值对了,将这行记录加入结果集;
  • 重复上一步,直到在 idxe2 上取到的值不是’zhangs’时,循环结束。
# 我们指定了前六个字节
alter table SUser add index index1(email(6));
# 查询
select id,name,email from SUser where email='[email protected]';

1.3 存在的问题

  • 两种方式对于空间的占用是不一样的

索引选取的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。

  • 我们需要合理的规定前缀长度

也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。

  • 规定长度对覆盖索引的影响

当查询的列都在二级索引中我们不需要回表查询,但由于你规定了长度,这时还是需要回表确认的,就算你定义的长度正好是邮箱的全部长度也是要回表进行确认的

  • 对于区分度不够好的字符串怎么办(例如身份证)
  • 采用倒序存储喽:不会额外占用空间,调用反转函数比hash函数资源消耗小一点,但是查询性能可能会相对来说不稳定,因为还是前缀
  • 使用hash字段:添加一个额外字段来存储散列值,不过有冲突的可能性,所以查询出来需要对身份证进行等值匹配,缺点有额外占用空间

为什么表数据删掉一半,表文件大小不变?

1.空洞

  • 概述

当删除表记录、表、数据库时,这些数据占用的空间只是被标记是删除的,但是实际上并没有删除,等待下一次符合条件的数据插入后来复用,此时这些空间就被称为空洞

  • 插入数据也会造成空洞

比如,如果按照主键递增的方式插入的,注意为啥不能将550留在pageA呢,此时就要说明页分裂是非常消耗资源的,同时又要满足主键递增排序,所以就会造成这样的结果
关于极客时间 | MySQL实战45讲的部分总结_第1张图片

1.1 如何处理空洞

  • 重建表

此时我们要把表A进行重建,第一步建立一个相同表结构的表B,然后进行顺序插入。由于表 B 是新建的表,所以表 A 主键索引上的空洞,在表 B 中就都不存在了。显然地,表 B 的主键索引更紧凑,数据页的利用率也更高。如果我们把表 B 作为临时表,数据从表 A 导入表 B 的操作完成后,用表 B 替换 A,从效果上看,就起到了收缩表 A 空间的作用。

  • 如何重建表

使用如下的语句mysql会为你自动完成转存数据、交换表名、删除旧表的操作。但是这个操作不支持并发,此时如果对原表A进行修改就会造成数据丢失。因此,在整个 DDL 过程中,表 A 中不能有更新。也就是说,这个DDL 不是 Online的。也就是说这个语句是强制转换,会在sever层创建一个临时表

alter table A engine=InnoDB
  • Online DDL

MySQL 5.6 版本开始引入的 Online DDL,对这个操作流程做了优化。这就是为了解决不能修改原表的问题,与前面的有所不同的是由于日志文件记录和重放操作这个功能的存在,这个方案在重建表的过程中,允许对表 A 做增删改操作。这也就是 Online DDL 名字的来源。

这里说一下注意事项,在进行Online时,能进行修改那肯定是进行了加锁操作,这个MDL写锁获取后会在真正开始拷贝数据之前退化成读锁,并且这个读锁不会阻塞插入、更新等DML语句,为什么不直接解锁,因为为了保护自己只允许当前线程进行,其他线程无法同时做DDL

1.2 Online和inplace

  • inplace

inplace的意思是在移动数据时,是原地操作,意思是对于server没有拷贝到临时表的操作。而是根据表 A 重建出来的数据是放在“tmp_file”里的,这个临时文件是 InnoDB 在内部创建出来的。整个 DDL 过程都在 InnoDB 内部完成。这跟不是Online的DDL的区别

  • online和inplace的关系
  • DDL 过程如果是 Online 的,就一定是 inplace 的;
  • 反过来未必,也就是说 inplace 的 DDL,有可能不是 Online 的。截止到 MySQL 8.0,添加全文索引(FULLTEXT index)和空间索引 (SPATIAL index) 就属于这种情况。

1.3 什么情况下会造成重建完表还变大的情况

  • 情况一

DDL 期间,如果刚好有外部的DML在执行,这期间可能会引入一些新的空洞。

  • 情况二

在重建表的时候,InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新用。也就是说,其实重建表之后不是“最”紧凑的。假如是这么一个过程:将表 t 重建一次;插入一部分数据,但是插入的这些数据,用掉了一部分的预留空间;这种情况下,再重建一次表 t,就可能会出现问题中的现象。

count(*)这么慢,我该怎么办?

1.count(*) 的实现方式

  • 概述

对于不同的存储引擎实现方式是不同的

  • MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高;如果加了 where 条件的话,MyISAM 表也是不能返回得这么快的。
  • InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。

2.为什么InnoDB也不把总行数存起来呢

  • 概述

这是因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的。

3.InnoDB对count(*)的优化

  • 针对索引的优化

对于InnoDB的表来说,聚簇索引的叶子结点存储的是一条记录的全部数据,而对于普通的索引来说叶子结点存储的是主键值等,相对于聚簇索引的体量小很多,所以在遍历记录数都会选择普通索引

  • show table status

还记命令吗,这个会把表的通用信息给展示出来,其中会有一个属性TABLE_ROWS显示这个表当前有多少行,但是你别忘了这个值是估算出来的。实际上,TABLE_ROWS 就是从这个采样估算得来的,因此它也很不准。有多不准呢,官方文档说误差可能达到 40% 到 50%。所以,show table status 命令显示的行数也不能直接使用。

4.所以怎么优化才能提高效率呢

  • 概述

这时就只能通过我们自己计数了,下面介绍几种方法

4.1 用缓存系统保存计数

  • 概述

对于更新很频繁的库来说,你可能会第一时间想到,用缓存系统来支持。你可以用一个 Redis 服务来保存这个表的总行数。这个表每被插入一行 Redis 计数就加 1,每被删除一行 Redis 计数就减 1。这种方式下,读和更新操作都很快

  • 缺点

缓存缓存,那么肯定有几率会造成数据的丢失。即使你通过方法解决了丢失的问题,但Redis就算能正常工作这个值还是逻辑上不精确的。例如下面两个时序图
关于极客时间 | MySQL实战45讲的部分总结_第2张图片
关于极客时间 | MySQL实战45讲的部分总结_第3张图片

4.2 在数据库保存计数

  • 概述

根据上面的分析,用缓存系统保存计数有丢失数据和计数不精确的问题。那么我们把这个计数直接放到数据库里单独的一张计数表 C 中,由于数据库的本身就支持崩溃恢复的功能,所以不用担心数据丢失的问题,但是对于计数不精准的问题,我们怎么解决

  • 通过事务

我们前面说过就是因为MVCC和事务的存在导致count(*)很慢,但对于这种情况我们就可以通过事务来反制,看如下时序图,会话B读取的计数值和记录都是会话A开启之前的旧数据所以互相不影响
关于极客时间 | MySQL实战45讲的部分总结_第4张图片

5.基于InnoDB不同的count用法效率

  • count是什么

对于不同的用法,我们还需要先搞清楚count到底是什么。count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加。最后返回累计值。

所以,count(*)、count(主键 id) 和 count(1) 都表示返回满足条件的结果集的总行数;而 count(字段),则表示返回满足条件的数据行里面,参数“字段”不为 NULL 的总个数。

  • 分析不同性能差别的原则
  • server 层要什么就给什么;
  • InnoDB 只给必要的值;
  • 现在的优化器只优化了 count(*) 的语义为“取行数”,其他“显而易见”的优化并没有做。
  • count(主键)

InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。

  • count(1)

InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。

  • count(主键)和count(1)

单看这两个用法的差别的话,你能对比出来,count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。

  • count(字段)

对于count字段分为两种情况,履行server需要什么就返回什么

  • 如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;
  • 如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。
  • count(*)

count(*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*) 肯定不是 null,按行累加。也许你会有疑问为啥count(主键)达不到跟count(*)一样的效率,优化器难道就不能将主键自动优化成*因为主键也是非空的啊,但是别忘了如果优化那就需要优化的门类太多了,还不如直接用count(*)来的实惠

order by 是怎么工作的?

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `city` varchar(16) NOT NULL,
  `name` varchar(16) NOT NULL,
  `age` int(11) NOT NULL,
  `addr` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `city` (`city`)
) ENGINE=InnoDB;

使用order by产生排序的情况

  • 概述

使用explain查看执行计划,Extra 这个字段中的“Using filesort”表示的就是需要排序,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer

1.全字段排序

  • 概述

当我们使用如下的查询,当一行的记录足够长,但还又没太长时,mysql会选择全字段排序。这个排序分为两类:一种是在内存中完成,另一种则需要借助排序文件在磁盘中完成

select city,name,age from t where city='杭州' order by name limit 1000  ;
1.1 完全在内存中完成
  • 取决因素

“按 name 排序”这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size。当要排序的数据量小于这个值,就会完全在内存中进行排序,你可以通过查看OPTIMIZER_TRACE 的结果来确认的,你可以从number_of_tmp_files中看到是否使用了临时文件。如果这个值等于0时就证明在内存中完成了排序

  • 流程图
    关于极客时间 | MySQL实战45讲的部分总结_第5张图片
1.2 借助磁盘进行排序
  • 概述

当排序的数据大于 sort_buffer_size这个值时,就会发现排序借助了磁盘的排序文件,同样你可以通过查看OPTIMIZER_TRACE 的结果来确认的,你可以从number_of_tmp_files中看到是否使用了临时文件。如果这个值大于0时就证明在内存中完成了排序。sort_buffer_size 越小,需要分成的份数越多,number_of_tmp_files 的值就越大。

可以看到为12,也就是说mysql将待排序的问分成12份,并适应归并排序最后合成一份关于极客时间 | MySQL实战45讲的部分总结_第6张图片

  • 其他参数解释
  • 我们的示例表中有 4000 条满足 city='杭州’的记录,所以你可以看到 examined_rows=4000,表示参与排序的行数是 4000 行。
  • sort_mode 里面的 packed_additional_fields 的意思是,排序过程对字符串做了“紧凑”处理。即使 name 字段的定义是 varchar(16),在排序过程中还是要按照实际长度来分配空间的。

2.rowid排序

  • 概述

上面的排序只对原表的数据读了一遍,剩下的操作都是在sort_buffer和临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。也就是说一行数据量太多,并且还有很多行,就要适用rowid来排序

  • 触发阈值

max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。

  • 执行查询语句

此时与全排序不同,加入内存参与排序的字段只会是name和id,下面看一下执行流程,可以看到多了一个回表操作,里面可能需要回表很多次

select city,name,age from t where city='杭州' order by name limit 1000  ;

关于极客时间 | MySQL实战45讲的部分总结_第7张图片

  • 结果分析

现在,我们就来看看结果有什么不同。首先,图中的 examined_rows 的值还是 4000,表示用于排序的数据是 4000 行。但是 select @b-@a 这个语句的值变成 5000 了。因为这时候除了排序过程外,在排序完成后,还要根据 id 去原表取值。由于语句是 limit 1000,因此会多读 1000 行。

  • sort_mode 变成了 ,表示参与排序的只有 name 和 id 这两个字段。
  • number_of_tmp_files 变成 10 了,是因为这时候参与排序的行数虽然仍然是 4000 行,但是每一行都变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应地变少了。
    关于极客时间 | MySQL实战45讲的部分总结_第8张图片

3.两种排序的对比

  • 概述
  • 如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
  • 如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。

对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。

可以看到mysql的策略就是能在内存中完成就在内存中完成

2.使用order by但没有进行排序的情况

  • 概述

当你合理进行索引的设置,比如你建立了(city,name)的联合索引,那么直接查到范围,回表查别的字段就好了

  • 流程图

可以看到流程省略了很多
关于极客时间 | MySQL实战45讲的部分总结_第9张图片

  • 查看执行计划

从图中可以看到,Extra 字段中没有 Using filesort 了,也就是不需要排序了。而且由于 (city,name) 这个联合索引本身有序,所以这个查询也不用把 4000 行全都读一遍,只要找到满足条件的前1000条记录就可以退出了。也就是说,在我们这个例子里,只需要扫描 1000 次。
在这里插入图片描述

如何正确的使用随机排序

  • 可以通过查看慢查询语句,来看扫描的行数之类的
    慢查询日志的开启方式

1.order by rand()

  • 概述

当有类似根据用户的学过的单词库今日随机推荐几个单词时,随着单词库越来越大效率会越来越低,一般这种的查询采用的是order by rand(),这个语句需要 Using temporary 和 Using filesort(Using temporary,表示的是需要使用临时表;Using filesort,表示的是需要执行排序操作。),查询的执行代价往往是比较大的。所以,在设计的时候你要尽量避开这种写法。

mysql> select word from words order by rand() limit 3;
  • 过程分析

默认有10000条数据,每行数据仅有id和word两个字段。首先mysql会在全字段排列或者rowid排列中挑选一个,对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。优化器没有了这一层顾虑,那么它会优先考虑的,就是用于排序的行越小越好了,所以,MySQL 这时就会选择 rowid 排序。

  1. 创建一个临时表。这个临时表使用的是memory引擎,表里有两个字段,第一个字段是 double 类型,为了后面描述方便,记为字段 R,第二个字段是varchar(64)类型,记为字段 W。并且,这个表没有建索引。
  2. words 表中,按主键顺序取出所有的word值。对于每一个 word 值,调用 rand() 函数生成一个大于 0 小于 1 的随机小数,并把这个随机小数和word分别存入临时表的R 和 W字段中,到此,扫描行数是 10000
  3. 现在临时表有 10000 行数据了,接下来你要在这个没有索引的内存临时表上,按照字段R排序。
  4. 初始化 sort_buffersort_buffer 中有两个字段,一个是double类型,另一个是整型。
  5. 从内存临时表中一行一行地取出R值和位置信息(位置信息就是rowid,因为是没有索引的表,行数据中会自动生成这个隐藏列),分别存入 sort_buffer 中的两个字段里。这个过程要对内存临时表做全表扫描,此时扫描行数增加 10000,变成了 20000
  6. sort_buffer 中根据 R 的值进行排序。注意,这个过程没有涉及到表操作,所以不会增加扫描行数。
  7. 排序完成后,取出前三个结果的位置信息,依次到内存临时表中取出word值,返回给客户端。这个过程中,访问了表的三行数据,总扫描行数变成了 20003
  • 注意

在mysql5开头写个扫描行数是20003,但在mysql8.0会变成10003,也就是说8版本对排序过程进行了优化

  • 总结

order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。

2.磁盘临时表

  • 概述

上面介绍过orderby的工作原理提到过这么一个参数tmp_table_size(默认为16M),让内存临时表超过这个值会转变为磁盘临时表来进行业务处理,我们可以通过执行完语句查看optimizer trace,具体的执行优化都在里面

  • 优先队列排序(堆排序)

在上面那个场景如果内存临时表超出也不会用到磁盘临时表,这时因为mysql5.6引入了优先队列排序,我们可以设置堆的大小使之全部数据对比完里面的数据就是排好序的规定数量的升序或降序(看你用大根堆还是小根堆)

  • 归并和堆排序

mysql在rowid中默认采用的是归并排序,我们的需求是在10000个中随机取出几个,使用归并排序后前几个取出,但是后面的9000多个都是排好序但用不到的,非常浪费资源。但如果使用堆排序规定容量对比完全部数据后是没有多余数据的

3.随机排序算法

  • 概述

上面介绍的sql写法无论怎么优化执行都是十分缓慢的,那么我们自己可以在sql中自己规划随机排序算法(或在业务层面规定,而不再查询层)

  • 例子

由于 limit 后面的参数不能直接跟变量,所以我在上面的代码中使用了 prepare+execute 的方法。你也可以把拼接 SQL 语句的方法写在应用程序中,会更简单些。MySQL 处理 limit Y,1 的做法就是按顺序一个一个地读出来,丢掉前Y个,然后把下一个记录作为返回结果,因此这一步需要扫描Y+1行。再加上,第一步扫描的 C 行,总共需要扫描C+Y+1行。这个执行代价如果进行计算的话其实在某些情况下会跟order by rand差不过,但是为什么效率会高?是因为我们查询数据使用到了索引,比使用rowid+临时表的要快上不少

mysql> select count(*) into @C from t;
set @Y = floor(@C * rand());
set @sql = concat("select * from t limit ", @Y, ",1");
prepare stmt from @sql;
execute stmt;
DEALLOCATE prepare stmt;

为什么sql逻辑一样但执行效率却不同

1.前言

我们知道当对条件进行函数修饰时,无论如何都使用不到索引,即排序列使用了复杂的表达式要想使用索引进行排序操作,必须保证索引列是以单独列的形式出现,而不是修饰过的形式,比方说这样:使用了UPPER函数修饰过的列就不是单独的列啦,这样就无法使用索引进行排序啦。

SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10;

其底层原理就是索引是B+数的结构,那么如果你使用函数传入一个值,会破坏树每一层的有序性,例如下面第一条语句,会消耗很长的时间去t_modified这个索引进行全局遍历,因为mysql根据7一下子就傻了;而不是根据这个索引建立的有序性一下就能找到

mysql> select count(*) from tradelog where month(t_modified)=7;
mysql> select count(*) from tradelog where t_modified='2018-7-1

关于极客时间 | MySQL实战45讲的部分总结_第10张图片

2.隐式类型转换

  • 概述

如下sql语句,在进行查询时你会发现进行了全表扫描,明明有索引。这时因为在mysql中字符串跟数字进行比较会将字符串转换成数字来进行比较。看到下面第二句用到了函数,同理是使用不了索引的

# 注意我们tradeid的字段类型为varchar
mysql> select * from tradelog where tradeid=110717;

# 根据上面,也就是说sql语句会被优化成这样
mysql> select * from tradelog where  CAST(tradid AS signed int) = 110717;
  • 思考

如果反过来,id字段时整型,但是查询的时候条件使用字符串会使用到索引吗?

select * from tradelog where id="83126";

此时根据上面类型转换,字符串会转化成整型,但是由于这个字符串只有一个,只用转换这一次。不会像上面查询时,会对字段中符合最左条件的都进行转换从而放弃使用索引。对于id的转换,是从左到右进行转换,遇到不能转换的字符会默认将其本身和后面的全部转换成0,如果第一个字符就是0即全部成0

3.隐式字符编码转换

  • 概述

对于两个表又是会进行联合查询什么连接之类的,这时如果两个表的字符编码不一样同样是用不了索引的,同理sql语句隐式调用了函数进行字符串编码的转换,注意这里表达的意思是要具体看优化器认为最快,最不消耗资源的,如下面第一条sql会对每个detail表中的值都进行转换,那么自然而然就用不到log中的索引了,而第二个查询虽然也有函数转换但用到了索引,是因为只对条件右边的驱动表做函数还是会用到索引的

# 是从 tradelog 表中取 tradeid 字段,再去 trade_detail 表里查询匹配字段。因此,
# 我们把 tradelog 称为驱动表,把 trade_detail 称为被驱动表,把 tradeid 称为关联字段。
mysql> select d.* from tradelog l, trade_detail d where 
d.tradeid=l.tradeid and l.id=2; /*语句Q1*/

select * from trade_detail  where 
CONVERT(traideid USING utf8mb4)=$L2.tradeid.value; 



# 这个语句里 trade_detail 表成了驱动表,但是 explain 结果的第二行显示,
# 这次的查询操作用上了被驱动表 tradelog 里的索引 (tradeid),扫描行数是 1。
mysql>select l.operator from tradelog l , trade_detail d where 
d.tradeid=l.tradeid and d.id=4;

select operator from tradelog  where 
traideid =CONVERT($R4.tradeid.value USING utf8mb4); 
  • 总结

字符集不同只是条件之一,连接过程中要求在被驱动表的索引字段上加函数操作,是直接导致对被驱动表做全表扫描的原因。

  • 优化方向

那么就显而易见的有两种:

  1. 就是建表就使用统一字符编码,也不用考虑什么驱动表和被驱动表了
  2. 就是避免被驱动表上使用函数操作

查一行也会很慢?

1.查询长时间不返回

等MDL锁

  • 概述

当形如这个的时候,我们就需要查询拿锁的这个线程时那个,然后kill掉就行(注意show processlist可看不出是哪个线程持有锁)在这里插入图片描述

  • 如何查看那个线程持有锁

有了 performance_schema 和 sys 系统库以后,就方便多了。(MySQL 启动时需要设置 performance_schema=on,相比于设置为 off 会有 10% 左右的性能损失)。通过查询 sys.schema_table_lock_waits 这张表,我们就可以直接找出造成阻塞的 process id,把这个连接用 kill 命令断开即可。
关于极客时间 | MySQL实战45讲的部分总结_第11张图片

等flush

  • 概述

在前面介绍缓冲池的时候,会有脏页集中刷新到磁盘的情况,此时用户线程将会被阻塞直到刷新完成,但是这种情况会很快,如果查询语句很长时间没有返回可能的原因是flush这个语句也被堵住了
在这里插入图片描述

  • MySQL 里面对表做 flush 操作的用法
  • flush tables t with read lock; 指定表的刷新
  • flush tables with read lock; 不指定,即刷新全部mysql的表

等行锁

  • 概述

例如以下场景B就会被阻塞,也许你会想使用MVCC中readView直接读取以前版本的数据就可以了啊,但是注意看语句,B中sql语句显式的说明自己需要共享锁并且B没有显式开启事务!!!,但此时的这个锁被A拿了(A拿了读写锁),所以就要进行等待关于极客时间 | MySQL实战45讲的部分总结_第12张图片

  • 解决

查出是谁占着这个写锁。如果你用的是 MySQL 5.7 版本,可以通过 sys.innodb_lock_waits 表查到。查询方法是:可以看到PId为4

mysql> select * from sys.innodb_lock_waits where locked_table='`test`.`t`'\G

关于极客时间 | MySQL实战45讲的部分总结_第13张图片

可以看到,这个信息很全,4 号线程是造成堵塞的罪魁祸首。而干掉这个罪魁祸首的方式,就是 KILL QUERY 4 或 KILL 4。不过,这里不应该显示“KILL QUERY 4”。这个命令表示停止 4 号线程当前正在执行的语句,而这个方法其实是没有用的。因为占有行锁的是 update 语句,这个语句已经是之前执行完成了的,现在执行 KILL QUERY,无法让这个事务去掉 id=1 上的行锁。实际上,KILL 4 才有效,也就是说直接断开这个连接。这里隐含的一个逻辑就是,连接被断开的时候,会自动回滚这个连接里面正在执行的线程,也就释放了 id=1 上的行锁。

2.查询慢

  • 场景描述

在一张id(主键)和age两个字段的表中,按顺序(从1,1开始)连续插入10000条记录后,开启两个线程会进行如下操作:
关于极客时间 | MySQL实战45讲的部分总结_第14张图片
可以看到生成了100万个undo日志,逻辑上未加锁的查询,需要从最新的undo日志一直寻找到适合自己的版本关于极客时间 | MySQL实战45讲的部分总结_第15张图片

  • 查询结果
    关于极客时间 | MySQL实战45讲的部分总结_第16张图片

  • 结果分析

没有加锁的查询使用的一致性读,也就是配合MVCC和readView来读取,由于事务未提交读取到最早的数据。加速的查询使用的当前读,因此会读到1000001这个结果

幻读问题究极解决方法

  • 幻读

我们说幻读问题的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新记录,我们把新插入的那些记录称之为幻影记录。

1.读操作利用MVCC,写操作加锁

  • 概述

此操作什么都好就是不能会当前读(也就是加了排他锁的读操作)进行限制,只会对一致性读(也就是没有任何修饰的查询)进行限制

2.读写操作都进行加锁

  • 概述

也就是无论你什么操作都进行加锁,这样能解决语义上的问题和数据一致性的问题

2.1 语义上的问题

  • 例子
CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
  • 概述

根据下图的执行表,当在Q1语句进行加锁后,在T2时刻还是会对id为0的值进行修改成功,原因是在 T1 时刻,session A 还只是给 id=5 这一行加了行锁, 并没有给 id=0 这行加上锁。虽然是后面修改的d,但是整体上还是破坏了加锁的语义
关于极客时间 | MySQL实战45讲的部分总结_第17张图片

2.2 数据一致性的问题

  • 下图时序上面发生的数据改变
  1. 经过 T1 时刻,id=5 这一行变成 (5,5,100),当然这个结果最终是在 T6 时刻正式提交的 ;
  2. 经过 T2 时刻,id=0 这一行变成 (0,5,5);
  3. 经过 T4 时刻,表里面多了一行 (1,5,5);
  • binlog中发生的改变,执行完的结果变为: (0,5,100)、(1,5,100) 和 (5,5,100)。跟我们上面期望的完全不一样,也就是说,id=0 和 id=1 这两行,发生了数据不一致。这个问题很严重,是不行的。也许你有疑惑为什么sessionA的更新在binlog中最后才执行,这是因为binlong日志是在commit提交时进行记录的
  1. T2 时刻,session B 事务提交,写入了两条语句;
  2. T4 时刻,session C 事务提交,写入了两条语句;
  3. T6 时刻,session A 事务提交,写入了 update t set d=100 where d=5 这条语句。
update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/

insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/

update t set d=100 where d=5;/*所有d=5的行,d改成100*/

关于极客时间 | MySQL实战45讲的部分总结_第18张图片

  • 原因分析及解决

这时因为在执行select * from t where d=5 for update 这句sql时是给d=5这一行加上了锁,也就是id=5这一行加了锁。如果我对扫描到的每一行都加锁,那么到T2时刻SessionB和C的操作会被阻塞到整个SessionA完全提交,而不是改变了但未提交导致实际生效的时间是最晚的

3.间隙锁(Gap lock)

  • 详情

转去看自己总结的!真的离谱 学了就忘

  • 前情回顾

对于上面的第二种方式你如果仔细分析查看,如果对每一行扫描到的数据都加锁对别的Session进行阻塞,binlog中执行顺序就会变成如下,可以看到对于id为0的幻读问题是解决了,但是对于id为1还是会变成(1,5,100),也就是说还是存在幻读问题

insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/

update t set d=100 where d=5;/*所有d=5的行,d改成100*/

update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/
  • 原因分析

对于全部加锁的解决方式还是有漏洞,原因是你对于id为1这条记录不可能存在限制的,因为加都没加进来你加什么锁,你根本不知道给谁加锁

3.1 间隙锁引入所带来的问题

3.1.1 死锁问题
  • sql语句如下
begin;
select * from t where id=N for update;

/*如果行不存在*/
insert into t values(N,N,N);
/*如果行存在*/
update t set d=N set id=N;

commit;
  • 死锁场景模拟

可以看到不用update就已经死锁了,原因是我们分析步骤就可以了,由于间隙锁的获取并不会冲突,谁来都可以获取,这就导致了死锁

  1. session A 执行 select … for update 语句,由于 id=9 这一行并不存在,因此会加上间隙锁 (5,10);
  2. session B 执行 select … for update 语句,同样会加上间隙锁 (5,10),间隙锁之间不会冲突,因此这个语句可以执行成功;
  3. session B 试图插入一行 (9,9,9),被 session A 的间隙锁挡住了,只好进入等待;
  4. session A 试图插入一行 (9,9,9),被 session B 的间隙锁挡住了。
    关于极客时间 | MySQL实战45讲的部分总结_第19张图片
  • 总结

间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

为什么改一条记录这么多锁?

1.加锁原则

  • 前言

根据不同的版本会发生些许变化

  • 概述
  • 原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 前开后闭区间。
  • 原则 2:查找过程中访问到的对象才会加锁(即加锁只会给对于涉及到正儿八经访问的索引加锁)。
  • 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁(前开后开)。
  • 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁(前开后开)。
  • 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止(mysql8.0.25已修复)。
  • 注意

在可重复读中采用幻读的问题,所以引入了间隙锁,但是在提交读的隔离下幻读问题是得到解决的,所以只有行锁没有间隙锁

2.根据加锁原则得到的优化

  • 概述
  • 删除尽量限制行数
  • 我们在分析加锁规则的时候可以用 next-key lock 来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的。

MySQL有哪些“饮鸩止渴”提高性能的方法?

  • 短风暴连接

正常的短连接模式就是连接到数据库后,执行很少的 SQL 语句就断开,下次需要的时候再重连。如果使用的是短连接,在业务高峰期的时候,就可能出现连接数突然暴涨的情况。

  • 风险

短连接模型存在一个风险,就是一旦数据库处理得慢一些,连接数就会暴涨。max_connections 参数,用来控制一个 MySQL 实例同时存在的连接数的上限,超过这个值,系统就会拒绝接下来的连接请求,并报错提示“Too many connections”。对于被拒绝连接的请求来说,从业务角度看就是数据库不可用。在机器负载比较高的时候,处理现有请求的时间变长,每个连接保持的时间也更长。这时,再有新建连接的话,就可能会超过 max_connections 的限制。

  • 增加这个参数的限制?

不可能这样来,因为大量的资源耗费在权限验证等逻辑上,结果可能是适得其反,已经连接的线程拿不到 CPU 资源去执行业务的 SQL 请求。

1.先处理掉那些占着连接但是不工作的线程。

  • 概述

通过配合show processlist和查 information_schema 库的 innodb_trx 表,来断开事务外的空闲连接,之所以要看innodb_trx 表去确认那个具体是哪个线程,是因为两个语句如下图,在空闲后只查看show processlist是一模一样的,但删除两个连接的后果有不同
关于极客时间 | MySQL实战45讲的部分总结_第20张图片可以看到都是sleep,但明显我们需要将SessionB的连接取出掉
关于极客时间 | MySQL实战45讲的部分总结_第21张图片- 有损分析
从服务端断开连接使用的是kill connection + id 的命令, 一个客户端处于 sleep 状态时,它的连接被服务端主动断开后,这个客户端并不会马上知道。直到客户端在发起下一个请求的时候,才会收到这样的报错“ERROR 2013 (HY000): Lost connection to MySQL server during query”。从数据库端主动断开连接可能是有损的,尤其是有的应用端收到这个错误后,不重新连接,而是直接用这个已经不能用的句柄重试查询。这会导致从应用端看上去,“MySQL 一直没恢复”

2.减少连接过程的消耗。

  • 概述

就是通过加入参数–skip-grant-tables,来启动数据库,整个 MySQL 会跳过所有的权限验证阶段,包括连接过程和语句执行过程在内。这是非常危险的及其不推荐

3.慢查询导致的性能问题及解决方法

  • 概述

导致慢查询的原因大体由下面三种

  1. 索引没有设计好;
  2. SQL 语句没写好;
  3. MySQL 选错了索引。

3.1 索引没有设计好

  • 概述

在这种情况下效率最高的补救措施就是直接利用Online DDL进行添加索引的操作,例如一主一备的过程如下:

  1. 在备库 B 上执行 set sql_log_bin=off,也就是不写 binlog,然后执行 alter table 语句加上索引;
  2. 执行主备切换;
  3. 这时候主库是 B,备库是 A。在 A 上执行 set sql_log_bin=off,然后执行 alter table 语句加上索引。
  • 总结

这是一个“古老”的 DDL 方案。平时在做变更的时候,你应该考虑类似 gh-ost 这样的方案,更加稳妥。但是在需要紧急处理时,上面这个方案的效率是最高的。

3.2 SQL 语句没写好

  • 概述

SQl语句没有写好导致的没有正确使用索引,我们可以通过提供了 query_rewrite 功能(5.7加入),可以把输入的一种语句改写成另外一种模式。比如,语句被错误地写成了 select * from t where id + 1 = 10000,你可以通过下面的方式,增加一个语句改写规则。


mysql> insert into query_rewrite.rewrite_rules
(pattern, replacement, pattern_database) values 
("select * from t where id + 1 = ?", "select * from t where id = ? - 1", "db1");

call query_rewrite.flush_rewrite_rules();

3.3 mysql自己选错了索引

  • 概述

这种情况下就去看看这篇博文的开头

3.4 总结

  • 概述

对于前两种情况我们是完全能够避免的

  • 过程
  1. 上线前,在测试环境,把慢查询日志(slow log)打开,并且把 long_query_time 设置成 0,确保每个语句都会被记录入慢查询日志;
  2. 在测试表里插入模拟线上的数据,做一遍回归测试;
  3. 观察慢查询日志里每类语句的输出,特别留意 Rows_examined 字段是否与预期一致。
  • 后续

如果新增的 SQL 语句不多,手动跑一下就可以。而如果是新项目的话,或者是修改了原有项目的 表结构设计,全量回归测试都是必要的。这时候,你需要工具帮你检查所有的 SQL 语句的返回结果。比如,你可以使用开源工具 pt-query-digest(https://www.percona.com/doc/percona-toolkit/3.0/pt-query-digest.html)

4.QPS激增问题

  • 概述

有时候由于业务突然出现高峰,或者应用程序 bug,导致某个语句的 QPS 突然暴涨,也可能导致 MySQL 压力过大,影响服务。我之前碰到过一类情况,是由一个新功能的 bug 导致的。当然,最理想的情况是让业务把这个功能下掉,服务自然就会恢复。

  1. 一种是由全新业务的 bug 导致的。假设你的 DB 运维是比较规范的(万事做好准备,应对困难也会更加从容),也就是说白名单是一个个加的。这种情况下,如果你能够确定业务方会下掉这个功能,只是时间上没那么快,那么就可以从数据库端直接把白名单去掉。
  2. 如果这个新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开现有连接。这样,这个新功能的连接不成功,由它引发的 QPS 就会变成 0。
  3. 如果这个新增的功能跟主体功能是部署在一起的,那么我们只能通过处理语句来限制。这时,我们可以使用上面提到的查询重写功能,把压力最大的 SQL 语句直接重写成"select 1"返回。

    对于这一种解决方式,会有两种后果:

    • 如果别的功能里面也用到了这个 SQL 语句模板,会有误伤;
    • 很多业务并不是靠这一个语句就能完成逻辑的,所以如果单独把这一个语句以 select 1 的结果返回的话,可能会导致后面的业务逻辑一起失败。

5.类似饮鸩止渴的场景

关于极客时间 | MySQL实战45讲的部分总结_第22张图片

binlog

WAL

!!!!
关于极客时间 | MySQL实战45讲的部分总结_第23张图片

1.binlog写入机制

  • 概述

其实,binlog 的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中(这里牢记就是写在了文件管理中page cache中即还是在内存,没有进行同步。这个过程就是write)。
一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。

  • binlog_cache_size

系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。

  • 事务提交的binlog的过程

事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache。
关于极客时间 | MySQL实战45讲的部分总结_第24张图片

  • 状态图分析(fsync函数同步内存中所有已修改的文件数据到储存设备)

可以看到,每个线程有自己 binlog cache,但是共用同一份 binlog 文件。

  • 图中的 write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快。
  • 图中的 fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为 fsync 才占磁盘的 IOPS。write 和 fsync 的时机,是由参数 sync_binlog 控制的:
  • sync_binlog
  • sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
  • sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
  • sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
  • sync_binlog的设置推荐

因此,在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0,比较常见的是将其设置为 100~1000 中的某个数值。

但是,将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。

2.配合redo log的参数设置以优化IO带来的性能瓶颈问题

  • redo log的相关参数

innodb_flush_log_at_trx_commit:

  • 0 :当该系统变量值为0时,表示在事务提交时不立即向磁盘中同步 redo 日志,这个任务是交给后台线程做的。这样很明显会加快请求处理速度,但是如果事务提交后服务器挂了,后台线程没有及时将 redo 日志刷新到磁盘,那么该事务对页面的修改会丢失。所以不建议这样做
  • 1 :当该系统变量值为1时,表示在事务提交时需要将 redo 日志同步到磁盘,可以保证事务的 持久性 。 1也是 innodb_flush_log_at_trx_commit 的默认值。
  • 2 :当该系统变量值为2时,表示在事务提交时需要将 redo 日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。
  • bin log的相关参数
  • binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用
  • fsync;binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync。
    这两个条件是或的关系,也就是说只要有一个满足条件就会调用 fsync。

sync_binlog:上面我们提到过

2.1 优化参考

  1. 设置 binlog_group_commit_sync_delaybinlog_group_commit_sync_no_delay_count 参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
  2. sync_binlog 设置为大于1的值(比较常见是 100~1000)。这样做的风险是,主机掉电时会丢 binlog 日志。
  3. innodb_flush_log_at_trx_commit设置为 2。这样做的风险是,主机掉电的时候会丢数据。

MYSQL是如何保证主备一致的?

  • 核心:binlog

binlog 可以用来归档,也可以用来做主备同步

1.主备一致的基本原理

  • 主备切换基本流程图

访问A节点(主库),为何推荐B节点(备库)设置为只读,反过来同理,不用担心设置成已读还怎么跟主库保持同步(不同步的线程拥有最高权限不会被只读限制)

  1. 有时候一些运营类的查询语句会被放到备库上去查,设置为只读可以防止误操作;
  2. 防止切换逻辑有 bug,比如切换过程中出现双写(即在两个库进行写操作),造成主备不一致;
  3. 可以用 readonly 状态,来判断节点的角色。
    关于极客时间 | MySQL实战45讲的部分总结_第25张图片
  • 节点A到节点B的内部流程

A节点和B节点之前保持着长连接(其中IO线程负责连接管理)

  1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
  2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_threadsql_thread。其中io_thread负责与主库建立连接。
  3. 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
  4. 备库 B 拿到binlog后,写到本地文件,称为中转日志(relay log)。
  5. sql_thread 读取中转日志,解析出日志里的命令,并执行。
    关于极客时间 | MySQL实战45讲的部分总结_第26张图片

2.binlog

  • 概述

binlog有三个种类,statement、row、mixed(前两种的混合体)

2.1 statement格式的binlog

只记录修改数据的SQL

  • 不需要记录每一行的变化,减少了binlog日志量,文件较小
  • binlog中包含了所有数据库更改信息,可以据此来审核数据库的安全等情况
  • binlog可以用于实时的还原,而不仅仅用于复制
  • 主从版本可以不一样,从服务器版本可以比主服务器版本高
  • 建表
mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `a` (`a`),
  KEY `t_modified`(`t_modified`)
) ENGINE=InnoDB;

insert into t values(1,1,'2018-11-13');
insert into t values(2,2,'2018-11-12');
insert into t values(3,3,'2018-11-11');
insert into t values(4,4,'2018-11-10');
insert into t values(5,5,'2018-11-09');
  • 执行语句后的binlog(可以用过mysql自带的binlog工具来看具体细节)
  • 第一行以后会提
  • 第二行事务开始,对于第四行
  • 第三行正儿八经执行语句,如图会全部记录下来,前面的user xx表示binlog的默认行为,可以保证准确无误的同步表
  • 第四句结束事务,并显示xid(xid是连接redo log和binlog之间的桥梁,通过xid就可以看到事务的提交状态)
    在这里插入图片描述
# 注意,下面这个语句包含注释,如果你用 MySQL 客户端来做这个实验的话,
# 要记得加 -c 参数,否则客户端会自动去掉注释。
mysql> delete from t /*comment*/  where a>=4 and t_modified<='2018-11-10' limit 1;

mysql> show binlog events in 'master.000001';
  • 该语句执行完成会有warnings

原因是当前格式是statement,并且语句中有limit,可能会导致在主库与备库执行有歧义即使用的优化方式可能会不同关于极客时间 | MySQL实战45讲的部分总结_第27张图片

  • 其他缺点
  • 上面所说不能复制有歧义的函数
  • INSERT … SELECT 会产生比 row 更多的行级锁
  • 复制需要进行全表扫描(WHERE 语句中没有使用到索引)的 UPDATE 时,需要比 row 请求更多的行级锁
  • 对于有 AUTO_INCREMENT 字段的 InnoDB表而言,INSERT 语句会阻塞其他 INSERT 语句
  • 对于一些复杂的语句,在从服务器上的耗资源情况会更严重,而 row 模式下,只会对那个发生变化的记录产生影响
  • 执行复杂语句如果出错的话,会消耗更多资源
  • 数据表必须几乎和主服务器保持一致才行,否则可能会导致复制出错

2.2 row格式的binlog

  • 概述

sql语句跟上面一模一样只是更改格式为row,可以看到发生变化的是具体执行语句,即记录修改了那些数据以及旧数据是什么
binlog的样子如下::

  • Table_map event,用于说明接下来要操作的表是 test 库的表 t;
  • Delete_rows event,用于定义删除的行为。关于极客时间 | MySQL实战45讲的部分总结_第28张图片
  • 使用mysql专门分析binlog的工具显示具体信息

从其中的信息看到,row格式的binlog会具体记录语句执行完的结构,记录那条变动记录的相关信息,这样也就不会发生歧义

mysqlbinlog  -vv data/master.000001 --start-position=8900;

关于极客时间 | MySQL实战45讲的部分总结_第29张图片

  • 优点
  • 任何情况都可以被复制,这对复制来说是最安全可靠的。(比如:不会出现某些特定情况下的存储过程、function、trigger的调用和触发无法被正确复制的问题)
  • 多数情况下,从服务器上的表如果有主键的话,复制就会快了很多
  • 复制以下几种语句时的行锁更少:INSERT … SELECT、包含 AUTO_INCREMENT 字段的 INSERT、没有附带条件或者并没有修改很多记录的 UPDATE 或 DELETE 语句执行 INSERT,UPDATE,DELETE 语句时锁更少
  • 从服务器上采用多线程来执行复制成为可能
  • 缺点
  • binlog 大了很多
  • 复杂的回滚时 binlog 中会包含大量的数据
  • 主服务器上执行 UPDATE 语句时,所有发生变化的记录都会写到 binlog 中,而 statement 只会写一次,这会导致频繁发生 binlog 的并发写问题
  • 无法从 binlog 中看到都复制了些什么语句

2.3 mixed格式的binlog

  • 概述

row虽然准确无误,但是占用的空间太大,占用空间大写binlog也会占用资源,但statement对于某些语句有可能产生歧义。所以mysql想出了折中方案,会自动识别执行的语句可能会引发的问题,存在歧义就用row,不存在就用statement

2.4 设置推荐

  • 概述

现在越来越多的场景要求把 MySQL 的 binlog 格式设置成 row。这么做的理由有很多,我来给你举一个可以直接看出来的好处:恢复数据

对于更新类的语句(插入、删除、更新),row格式记录的被影响的行的变化,在查询后直接进行关键字替换或者记录语句的顺序替换都能够直接回复,值得一提的就是update语句在binlog下会保存更新前和更新后的整行数据

  • set timestamp

这个语句是在做更新语句是都会加上的语句,使语句独一份,保证了主备一致

  • 回复推荐

不建议直接将binlog通过细节展示得到的执行语句直接执行,因为有些语句执行是依赖与上下文的。所以,用 binlog 来恢复数据的标准做法是,用 mysqlbinlog 工具解析出来,然后把解析结果整个发给 MySQL 执行。类似下面的命令:

mysqlbinlog master.000001  --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;

3.循环复制的问题

  • 概述

现在应用越多的是双M结构,而本章开始的那个基本流程图是M-S结构,也就是同一时刻内单向主备关系,当主备关系切换时,需要切换标志位谁是主谁知备。而如下图所示就是双M结构:
关于极客时间 | MySQL实战45讲的部分总结_第30张图片

  • 优点

节点 A 和 B 之间总是互为主备关系。这样在切换的时候就不用再修改主备关系。

3.1 循环问题概述

  • 概述

前面我们展示过内部流程,提到过主库如果发生改变将生成的binlog发给备库,备库 B 拿到binlog后,写到本地文件,称为中转日志(relay log)。备库在根据binlog将改变同步到自己,但这个过程同样需要写到binlog中啊,因为是互为主备

  • 解决方式

这种问题的解决方式很千篇一律,添加一个标志为就行了,前面我们看到binlog日志中有一个serverid,表示谁记录的这个,那么解决流程逻辑就可以这样:

  • 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
  • 一个备库接到 binlog 并在重放的过程中,生成与原 binlogserver id 相同的新的 binlog
  • 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。

MYSQL如何保证高可用

0.MYSQL主从复制(集群)的常见解决方案

  1. MYSQL-MMM(MYSQL主主复制监视器)

分别给三个主机虚拟IP,监视器维护一个IP集,现在实际上是一主多从,只不过当真正的主机宕机,另外的作为备用主机;置换过程其实就是虚拟IP的更换,外界看着没变其实内部已经发生变化
关于极客时间 | MySQL实战45讲的部分总结_第31张图片

  1. MHA((Master High Availability)

在MySQL故障切换过程中,MHA 能做到在0~30秒之内自动完成数据库的故障切换操作(以2019年的眼光来说太慢了),并且在进行故障切换的过程中,MHA能在最大程度上保证数据的一致性,以达到真正意义上的高可用。

  1. InnoDB Cluster

InnoDB, Cluster支持自动Failover、强一致性、读写分离、读库高可用、读请求负载均衡,横向扩展的特性,是比较完备的一套方案。但是部署起来复杂,想要解决 router单点问题好需要新增组件,如没有其他更好的方案可考虑该方案。InnoDB, Cluster主要由MySQL Shell、MySQL Router 和 MySQL服务器集群组成,三者协同工作,共同为MySQL,提供完整的高可用性解决方案。MySQL. Shell对管理人员提供管理接口,可以很方便的对集群进行配置和管理.MySQL. Router可以根据部署的集群状况自动的初始化,是客户端连接实例。如果有节点down 机,集群会自动更新配置。集群包含单点写入和多点写入两种模式。在单主模式下,如果主节点down掉,从节点自动替换上来,MySQL Router会自动探测,并将客户端连接到新节点。
关于极客时间 | MySQL实战45讲的部分总结_第32张图片

  • 总的架构图

但是可以看见整个系统的储存容量是由主节点决定的,也就是我们很难扩展,也很难维护单个很大的数据库的索引速度等,所以我们可以采用如下的解决方案

  1. 分片存储
  2. 分库分表关于极客时间 | MySQL实战45讲的部分总结_第33张图片

1.MYSQL导致主备延迟的原因

1.1 数据同步的关键时间点

下面说的延迟时间大多就是T3-T1的时间延迟(通过查看seconds_behind_master这个参数就可以知道延迟)。主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产 binlog 的速度要慢

  1. 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;
  2. 之后传给备库 B,我们把备库 B 接收完这个 binlog 的时刻记为 T2;
  3. 备库 B 执行完成这个事务,我们把这个时刻记为 T3

1.2 原因

  • 备机配置太差:提高配置
  • 备机压力大:主机负责写,备机负责读;一主多从可以解决
  • 大事务:binlog负责备机同步,那么提交事务才能传输binlog,你一个事务(批量删除万万不可)整了个10分钟那完蛋
  • 大表的DDL
  • 备机的并行复制能力

2.优化策略

关于极客时间 | MySQL实战45讲的部分总结_第34张图片

2.1 可靠性优先策略

  • 概述

对于上述状态1~2的转换,我们可以描述步骤为,这个不步骤就是可靠性优先

  1. 判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步(这一步设置为第一步的目的就是拉低这个第三步的等待时间,因为2~3这两步主备都是只读状态);
  2. 把主库 A 改成只读状态,即把 readonly 设置为 true;
  3. 判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;
  4. 把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
  5. 把业务请求切到备库 B。

2.2 可用性优先策略

  • 概述

可用性就是把上述的可靠性的4~5步直接提到前面执行,不等同步让备库直接上来接收业务处理,这样几乎没有可用时间,也就造成了主备库数据不统一不一致

  • 数据不一致场景重现
    关于极客时间 | MySQL实战45讲的部分总结_第35张图片

因为 row 格式在记录 binlog 的时候,会记录新插入的行的所有字段值,所以最后只会有一行不一致。而且,两边的主备同步的应用线程会报错 duplicate key error 并停止。也就是说,这种情况下,备库 B 的 (5,4) 和主库 A 的 (5,5) 这两行数据,都不会被对方执行。
关于极客时间 | MySQL实战45讲的部分总结_第36张图片

2.3 总结

  • 概述

在满足数据可靠性的前提下,MySQL 高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。

3.备机的并行复制能力演变

  • 概述

在mysql5.6之前只支持备库的单线程复制

3.1 引入mysql复制的两种复制策略

  • 按表分发策略

给每个工作线程创建一个专属的hash来分配对应处理表,对于处理冲突问题,实行有冲突就等待,没有冲突就分配的策略
关于极客时间 | MySQL实战45讲的部分总结_第37张图片

  • 按行分发策略

按行则要要求binlog日志格式为row,不然你不知道事务影响了那些行,hash表中所要记录的信息也多(主键、唯一键、表名等)

  • 两者的对比

可以看到这种处理模型就是池化思想,并发且异步操作专人做专事。按行相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。如果又热点行或者表都会造成资源分配不统一的问题

  • 两者的约束
  • 要能够从 binlog 里面解析出表名、主键值和唯一索引的值。也就是说,主库的 binlog 格式必须是 row;
  • 表必须有主键;
  • 不能有外键。表上如果有外键,级联更新的行不会记录在 binlog 中,这样冲突检测就不准确。

3.2 MySQL 5.6 版本的并行复制策略

  • 概述

简单来说就是按库分发

3.3 MariaDB 的并行复制策略

  • 概述

利用redolog组提交的策略,能够在同一组里提交的事务,一定不会修改同一行;主库上可以并行执行的事务,备库上也一定是可以并行执行的。那么给同一个组的所有事务标上统一标号在一起执行就可了

  • 缺点

由于事务执行具有原子性,所以一组一组的执行会拉低效率

3.4 MySQL 5.7 的并行复制策略

  • 概述

对于MariaDB 的优化

  • 同时处于 prepare 状态的事务,在备库执行时是可以并行的;
  • 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的。

3.5 MySQL 5.7.22 的并行复制策略

  • 概述

对于按行、按表思想的实现

主库出问题了,从库怎么办

  • 概述

在现在这个互联网应用场景大多都是读多写少,也就是只读状态的备机会很多,即一主多从的结构

  • 一主多从的结构图

图中,虚线箭头表示的是主备关系,也就是 A 和 A’互为主备, 从库B、C、D指向的是主库 A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。关于极客时间 | MySQL实战45讲的部分总结_第38张图片

1.主库宕机后的主备切换

  • 宕机后的结构图

相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库 B、C、D 也要改接到 A’。正是由于多了从库 B、C、D 重新指向的这个过程,所以主备切换的复杂性也相应增加了。关于极客时间 | MySQL实战45讲的部分总结_第39张图片

1.1 切换流程

  • change master命令的参数

前四个参数代表了主库的IP、端口、用户名和密码;后两个标志着我们要从哪里开始同步即同步位点

  • MASTER_HOST=$host_name
  • MASTER_PORT=$port
  • MASTER_USER=$user_name
  • MASTER_PASSWORD=$password
  • MASTER_LOG_FILE=$master_log_name
  • MASTER_LOG_POS=$master_log_pos
  • 位点获得问题

由于A和A‘的位点肯定是不一样的,那么对于从库B等来说就要先找到A‘的位点在哪里,并且我们还要往前稍微找一点,在判断事务已经发生与否,对于位点的获取是不精准的,因为在发生情况的一瞬间可能某个语句已经执行完成,在同步给新的主库A‘时由于binlog的存在这行数据被成功的同步,但是由于我们这个位点的不精准,定位到同步数据之前的位置,并开始了一边同步,这时就会抛出异常,提示出现了主键冲突,然后停止同步

  • 异常解决
  1. 对于停止后,进行手动跳出此事务:sql_slave_skip_counter
  2. 设置遇到特定异常(一般就是主键冲突或删除数据找不到行)自动跳出:slave_skip_errors

1.2 异常解决的替代

  • 概述

对于上面两种方式都比较复杂,MySQl在5.6引入GTID(Global Transaction Identifier,也就是全局事务 ID),对于上面的替代就是提前将某条语句的全局ID通过空事务的方式提交到事务ID的集合上面,然后别的库一对照发现有了就不会暂停事务继续同步工作了

  • 优点

有了全局事务ID的分配,无论是将某个库的binlog关闭一段时间,往后在同步也不会造成数据不统一的情况,对比全局ID缺啥补啥就行了

2.读写分离的坑

  • 读写分离的大致做法

一共有两种:一是客户端直连,二是通过一个代理的角色分发请求(趋势)

  • 读写分离延迟问题

无论采用那种方案,都会有主从延迟,也就是当在从库上面读到了一个标志过期读的记录怎么办?一般有以下六种处理方式:

  1. 强制走主库方案;
  2. sleep 方案:睡个一秒大概,等新的数据到,然后读。以卖家发布商品为例,商品发布后,用 Ajax(Asynchronous JavaScript + XML,异步 JavaScript 和 XML)直接把客户端输入的内容作为“新的商品”显示在页面上,而不是真正地去数据库做查询。这样,卖家就可以通过这个显示,来确认产品已经发布成功了。等到卖家再刷新页面,去查看商品的时候,其实已经过了一段时间,也就达到了 sleep 的目的,进而也就解决了过期读的问题。
  3. 判断主备无延迟方案:一般有三种,一是直接看有个参数seconds_behind_master,只要这个参数不等于0就不执行,二是对比位点确保主备无延迟;三是对比全局事务ID集合
  4. 配合 semi-sync(半同步复制) 方案:类似于握手
  5. 等主库位点方案;
  6. 等 GTID 方案。

误删数据后除了跑路,还能怎么办?

  • 误删的分类
  1. 使用 delete 语句误删数据行;
  2. 使用 drop table 或者 truncate table 语句误删数据表;
  3. 使用 drop database 语句误删数据库;
  4. 使用 rm 命令误删整个 MySQL 实例。
  • 大致方法
  1. 通过备份binlog和全量备份的操作寻找误删时间段的binlog进行找回
  2. 通过设置一个专门的延时备库,将主备之间的延迟更新设置成一个小时,如果主库误操作,那么备库还有一个小时的时间来察觉并找回

为什么还有kill不掉的语句?

  • MySQL中kill的两种命令
  • kill query + 线程ID:表示终止这个线程中正在执行的语句
  • kill (connection) + 线程ID:表示断开这个线程的连接,当然如果这个线程有语句正在执行,也是要先停止正在执行的语句的
  • kill并不是立马停止

kill 并不是马上停止的意思,而是告诉执行线程说,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”。其实,这跟 Linux 的 kill 命令类似,kill -N pid 并不是让进程直接停止,而是给进程发一个信号,然后进程处理这个信号,进入终止逻辑。只是对于 MySQL 的 kill 命令来说,不需要传信号量参数,就只有“停止”这个命令。

1.kill的过程

  • 概述

当执行该语句时,会做两件事:

  1. 把 对于会话的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY);
  2. 给 会话 的执行线程发一个信号。
  • 解释为什么要发信号

因为只改状态,会话并不知道要终止,给个信号让其退出等待(假设该会话是死锁线程),处理kill状态

  • 过程
  1. 一个语句执行过程中有多处“埋点”,在这些“埋点”的地方判断线程状态,如果发现线程状态是 THD::KILL_QUERY,才开始进入语句终止逻辑;
  2. 如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到“埋点”处;
  3. 语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的。

2.出现killed(杀不掉)的两种情况

  1. 陷入等待进入InnoDB循环,无法判断检查自身的状态。即:线程没有执行到判断线程状态的逻辑。跟这种情况相同的,还有由于 IO 压力过大,读写 IO 的函数一直无法返回,导致不能及时判断线程的状态。
  2. 终止逻辑耗时较长。这时候,从 show processlist 结果上看也是 Command=Killed,需要等到终止逻辑完成,语句才算真正完成。这类情况,比较常见的场景有以下几种:
  • 超大事务执行期间被 kill。这时候,回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长。
  • 大查询回滚。如果查询过程中生成了比较大的临时文件,加上此时文件系统压力大,删除临时文件可能需要等待 IO 资源,导致耗时较长。
  • DDL 命令执行到最后阶段,如果被 kill,需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久。

3.两个客户端的误解

  1. 库中的表数量很多,连接会变慢?

对于我们感知的时间变慢,其实不是连接变慢了,对于客户端和服务端最常用的连接方式就是TCP/IP,对于单纯的网络连接根本不会受表的数量影响,那让我们感知到的其实是客户端根据库名和表明建立本地hash的过程,这个过程对应的功能体现就是按tab自动补全,想要不建立这个表在使用命令时加上 -A即可跳过

  1. -quick并没有让服务器变快

首先说明-quick的作用跟-A重合并还有两个功能

  1. mysql_store_result 需要申请本地内存来缓存查询结果,如果查询结果太大,会耗费较多的本地内存,可能会影响客户端本地机器的性能;
  2. 是不会把执行命令记录到本地的命令历史文件。

解释上面两个功能,还得先说以下客户端接收服务端结果的两种方式,当使用-quick时就会不建立缓存从而影响服务端的处理速度。即-quick只是让客户端变快了而已

  • 一是在本地建立缓存存放,
  • 二是不建立缓存,来一个处理一个

我查这么多数据,会不会把数据库内存打爆?

  • 概述

当对超大内存的库进行全表查询时会不会把主机数据库的内存全部撑爆?

1.客户端和服务端如何进行查询

  • 流程
  1. 获取一行,写到 net_buffer(服务端的结果集存放位置) 中。这块内存的大小是由参数 net_buffer_length 定义的,默认是 16k。
  2. 重复获取行,直到 net_buffer 写满,调用网络接口发出去。
  3. 如果发送成功,就清空 net_buffer,然后继续取下一行,并写入 net_buffer。
  4. 如果发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送。
# 假设,我们现在要对一个 200G 的 InnoDB 表 db1. t,执行一个全表扫描。当然,你要把扫描结果保存在客户端,会使用类似这样的命令
mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file
  • 流程图

可以看到,无论是缓存存放的地方还是发送的本地栈都是有容量限制的,不可能增大,当存满了就停止写线程直到可以再写,即MySQL使用的策略是边读边发
关于极客时间 | MySQL实战45讲的部分总结_第40张图片

  • 如果故意在客户端不去读buffer中内容呢?

上一节我们讲到一个参数-quick开启后,会停止使用缓存,来一条读一条,那么当发的buffer满了,就会使服务端的速度骤减,所以还是建议使用MySQL默认的,保存到本地缓存中

  • 如果是一个大查询呢

所谓大查询就是返回的结果集很大很多,也并不会把服务端的内存挤爆,因为有LRU链表的存在,当内存满了要引入新的页,会把不经常使用的页从链表中剔除,而且这个LRU算法是经过优化的(分新生代和老年代)

join语句怎么优化

0.基于回表的优化

  • 概述

形如以下的语句,在查出a列的数据后,相对于聚簇索引来说是随机IO,因为二级索引a列是根据a排序的。一般我们认为在插入数据的时候都是按照主键递增的顺序插入的,那么为了抵消这个随机IO,我们可以将查出a列的结果集根据主键进行排序,然后在进行回表就是一个顺序IO了,这种优化方式叫做Multi-Range Read 优化

1.循环嵌套连接(NLJ)

  • 概述

就是对于最基本的连接处理算法,针对与驱动表通过where或筛选出符合条件的,然后一条一条循环取出这个结果集的记录,在被驱动表进行全表扫表的符合连接条件on的加入最终结果集,当然这里连接条件的被驱动表是存在索引的话,相当于进行优化了

  • 缺点

对于驱动表每次只对比一行数据,却要对被驱动表进行一次的全表扫描,然后周而复始,太浪费资源

  • BKA算法优化

那么根据MRR的优化思想,我们就把很多条甚至全部的符合条件的驱动表记录都加载到join buffer进行多值范围匹配,减少被驱动表的扫描次数。像BNL了,BKA 算法的优化要依赖于 MRR。

2.基于块的循环嵌套连接(BNL)

  • 概述

为了减少被驱动表的扫描次数,把驱动表全部或分段的加载到join buffer中,每一条被驱动表的记录一次性与多条的驱动表记录进行对比,相当于使用内存加速了

  • 当被驱动表是一个大的冷数据表

我们说过在内存的表会被记录在LRU表,LRU表又分为young和old区域,当新加入到缓冲池的页会加到old区的头部,当这个表1秒后再次被用到就会被加入到young区域的头部中,但如果这个表每个页的都被访问到,平均访问时间小于1秒就不会被优化到young区域中

那么我们再看如果这个被驱动表是一个冷数据表,正常数据要进入young的条件是先进入old区,但冷表数据很大,正常数据很快就被挤出old区,没有进入young区的机会,从而使热点数据一直从磁盘读取,同时young区的数据得不到更新,也就不再young了。

而且这个影响是要等到冷数据表彻底访问完以后,随着后续的正常访问命中率才会逐渐回复

  • BNL算法对系统的影响
  • 可能会多次扫描被驱动表,占用磁盘 IO 资源;
  • 判断 join 条件需要执行 M*N 次对比(M、N 分别是两张表的行数),如果是大表就会占用非常多的 CPU 资源;
  • 可能会导致 Buffer Pool 的热数据被淘汰,影响内存命中率。
  • 对BNL的优化

我们可以使用BKA算法进行优化,也就是在被驱动表上加上索引,符合了MRR也就符合了BKA,从无序变成有序了,但是注意为了一个频率很低的SQL为一个表建一个几乎用的很少的索引是不值得的

3.hash存储

  • 概述

我们前面的优化无非就是基于两个方向,将磁盘的数据直接放到内存中缓冲,用内存的速度提高整体的速度,要不就是将无序变得有序,随机IO变成顺序IO,那么对于内存中这个无序表,没必要进行排序直接建一个hash表存进去就好了,但是mysql官方是没有这个数据结构的,所以需要我们把记录查出来以后在业务端进行存入并查询

临时表

  • 概述

这个临时表可不是那个mysql内部在内存中进行暂存的内部临时表或者暂存排序结构的内部临时表,这个临时表使我们自发进行创建的,同样这个临时表也不是内存表,当你使用memory引擎时创建内存表时在内存中进行存储,每次重启数据清空表结构不变,而临时表,可以使用各种引擎类型 。如果是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,写数据的时候是写到磁盘上的。

1.临时表的特性

  • 根据如图
  • 可以看到创建语法是create temporary tablexxx
  • 临时表是对于本对话可见,本对话结束临时表自动删除
  • 临时表可以和普通表同名
  • show tables 不显示临时表
    关于极客时间 | MySQL实战45讲的部分总结_第41张图片
  • 适用于join的场景
  1. 不同 session 的临时表是可以重名的,如果有多个 session 同时执行 join 优化,不需要担心表名重复导致建表失败的问题。
  2. 不需要担心数据删除问题。如果使用普通表,在流程执行过程中客户端发生了异常断开,或者数据库发生异常重启,还需要专门来清理中间过程中生成的数据表。而临时表由于会自动回收,所以不需要这个额外的操作。

2.临时表的应用

  • 概述

在分库分表跨库查询中临时表经常使用,一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大表 ht,按照字段 f,拆分成 1024 个分表,然后分布到 32 个数据库实例上

  • sql语句
# 如果查询的是根据f列设置成分区键时,根据某个分表规则以下就是定位到表
select v from ht where f=N;
# 但如果查询列不是我们的分区键时,这时就会有大概两种策略
select v from ht where k >= M order by t_modified desc limit 100;
  1. 将全部的符合条件的记录放在内存中进行排序并限制记录数
  2. 创建一个临时表,将每个分表中符合条件的记录查出并插入到临时表,然后对临时表进行相同条件的查询即可得
    关于极客时间 | MySQL实战45讲的部分总结_第42张图片
  • 总结

我们一般采用第二种策略,因为第一种虽然速度快,在高并发的场景下太占资源

3.为什么临时表可以重名

  • 概述

对于临时表,在底层存储和mysql内部识别都是与普通表不一样的,所以可以重名

  • 总结

在实现上,每个线程都维护了自己的临时表链表。这样每次 session 内操作表的时候,先遍历链表,检查是否有这个名字的临时表,如果有就优先操作临时表,如果没有再操作普通表;在 session 结束的时候,对链表里的每个临时表,执行 “DROP TEMPORARY TABLE + 表名”操作。

4.临时表和主备复制

  • binlog

在binlog中对于临时表的各种操作都是会记录进去,上面说到当你在本对话结束要对临时表进行删除,因为在主备的关系中如果binlog的格式不是row,备库根据binlog也会执行创建临时表的各种操作。但是等你结束了没有删除,虽然主库的临时表随着结束而清空,但是备库可没有停止,所以我们需要这个命令同步到binlog随之删除备库的临时表

所以当为row时,你要删除某个表及临时表时,再发送到备库同步用的binlog会改写成删除普通表,因为备库就根本没有临时表

  • 同一session下不同线程创建同名临时表

这种情况下由于备库的binlog是写在一起的,也就是说会执行两遍创建同名临时表的操作,但由于我们说过临时表既然已经支持同名也对这个情况有相应的处理

4.内部临时表

  • 概述

现在就要介绍区别于自发创建的临时表,由mysql创建的内部临时表了,一定要区分哦

  • 是什么情况下会使用
  • union
  • group by
  • group by:通过索引优化

对于group by 的语义逻辑,是统计不同的值出现的个数。对于每一行来说可能是无序的,那么就可以使用索引避免使用临时表或磁盘表(当表十分大时超过了内存临时表的上限)和排序,

  • group by:直接排序优化

但如果某个场景不适合创建索引,我们还是只能采用老路来提高速度,当表的数据量十分大时,超过内存临时表的大小会转成磁盘数组,也就是有这个额外判断的过程,但如果我们提前就知道肯定会超过能不能跳过这个判断过程呢?

# SQL_BIG_RESULT就是直接使用磁盘数组存放,而不是用内存临时表
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;

4.1 什么使用能使用到内部临时表

  1. 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果;
  2. join_buffer 是无序数组,sort_buffer 是有序数组,临时表是二维表结构;
  3. 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中,union 需要用到唯一索引约束, group by 还需要用到另外一个字段来存累积计数。

4.2 总结

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

Memory引擎

  • 先来看看各大引擎对于某些功能的支持,默认临时表是使用 Memory 引擎的
    关于极客时间 | MySQL实战45讲的部分总结_第43张图片
    关于极客时间 | MySQL实战45讲的部分总结_第44张图片

1.内存表的数据组织结构

  • 执行语句,t1是memory可以看到0在结尾,t2是InnoDB0在开头
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);

关于极客时间 | MySQL实战45讲的部分总结_第45张图片

  • InnoDB的存储结构你已经很清楚了,根据主键顺序存储在B+树的叶子节点上

  • Memory的存储结构如下

可以看到,内存表的数据部分以数组的方式单独存放,而主键 id 索引里,存的是每个数据的位置。主键 id 是 hash 索引,可以看到索引上的 key 并不是有序的。所以当进行select *的时候走的是全表查询,依次扫描这个数据数组
关于极客时间 | MySQL实战45讲的部分总结_第46张图片

  • 两者在数据组织结构上的不同
  • InnoDB 引擎把数据放在主键索引上,其他索引上保存的是主键 id。这种方式,我们称之为索引组织表(Index Organizied Table)。
  • 而 Memory 引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表(Heap Organizied Table)
  • 到目前为止的典型不同
  • InnoDB 表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的;
  • 当数据文件有空洞的时候,InnoDB 表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值(复用);
  • 数据位置发生变化的时候,InnoDB 表只需要修改主键索引,而内存表需要修改所有索引;
  • InnoDB 表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的。
  • InnoDB 支持变长数据类型,不同记录的长度可能不同;内存表不支持 Blob 和 Text 字段,并且即使定义了 varchar(N),实际也当作 char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。

2.hash 索引和 B-Tree 索引

  • 概述

在Memory引擎中也是可以使用到B-tree索引的,执行如下的命令

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

关于极客时间 | MySQL实战45讲的部分总结_第47张图片

  • 对比

关于极客时间 | MySQL实战45讲的部分总结_第48张图片

  • 总结

一般在我们的印象中,内存表的优势是速度快,其中的一个原因就是 Memory 引擎支持 hash 索引。当然,更重要的原因是,内存表的所有数据都保存在内存,而内存的读写速度总是比磁盘快。

3.为什么线上不推荐你使用Memory

  • 概述

主要有两个原因:锁粒度问题;数据持久化问题。

3.1 锁粒度问题

  • 概述

对于Memory来说是没有行锁只有表锁的,也就是说当另一个会话想要修改表的数据就要先拿到此表的表锁。因此,一张表只要有更新,就会堵住其他所有在这个表上的读写操作。

  • 总结

即跟行锁比起来,在并发场景下会限制性能

3.2 数据持久化问题

  • 概述

前面说过Memory之所以快是因为数据都放在内存表中,双刃剑的体现就是每次重启,内存表会被清空

  • 在M-S结构下(即主备关系在同一时刻内是单向的)

在这个场景下,如果备机中间重启业务升级,回来在执行语句就会显示找不到记录了;如果进行的主备切换就更诡异了,主库进行重启会担心主备不一致,因为内存表会进行清空,那么为了一直MYSQL会在让主库在binlog上面写上删除对应的内存表,那么备库这个临时表也就没有了
关于极客时间 | MySQL实战45讲的部分总结_第49张图片

  • M-M结构下(双向)

在这个结构下,那么备库可以作为主库,主库也能作为备库,当备库进行重启时同样会在主库上复现那个诡异的操作
关于极客时间 | MySQL实战45讲的部分总结_第50张图片

3.3 总结

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

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

  • 临时表不会被其他线程访问,没有并发性的问题;
  • 临时表重启后也是需要删除的,清空数据这个问题不存在;
  • 备库的临时表也不会影响主库的用户线程。
    关于极客时间 | MySQL实战45讲的部分总结_第51张图片

自增键为什么不是连续的

1.自增键存储在哪里

  • MyISAM保存在数据文件中
  • MYSQL:在5.7及以前版本,自增键是没有持久化的,保存在内存中每次重启后都会找最大的值然后+1作为下次的;8.0版本之后就保存在redo log中了

2.自增值修改机制

  • 当某个列为是自增列,下一个自增值是下面的策略实现的
  1. 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;
  2. 如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。根据要插入的值与自增值的大小关系,自增值会得到相对应的更新;如果插入值小于当前自增值,自增值不变;如果大于等于则修改当前自增值为新的自增值
  • 新的自增值的生成策略

auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 X 的值,作为新的自增值。

当一主多从,要求多写时,会让auto_increment_increment 设置成2,防止备库的主键id冲突

auto_increment_offset 和 auto_increment_increment 都是 1 的时候,新的自增值生成逻辑很简单,就是:

  • 如果准备插入的值 >= 当前自增值,新的自增值就是“准备插入的值 +1”
  • 否则,自增值不变

3.为什么自增值不连续

3.1 唯一键冲突导致自增值不连续(第一种原因)
  • 概述

这个原因主要就是自增值的赋值和自增行为在插入记录时是处于发现冲突之前的,也就是因为唯一键导致插入失败,但是自增值已经增长了,下一次插入记录就会发现自增值不连续了

3.2 事务回滚导致不连续(第二种原因)
  • 形如以下sql语句
insert into t values(null,1,1);
begin;
# 这句回滚后,自增值已经完成自增
insert into t values(null,2,2);
rollback;
insert into t values(null,2,2);
//插入的行是(3,2,2)
3.3 自增值为什么不能回退
  • 概述

也许你会想为什么回滚事务的时候,不会自增值也给设置成原先的呢?MySQL这样设置的原因是为了提高性能

  • 场景描述

在多并发的场景下,如果两个事务并发的执行,一步一步的进行完成自增键逻辑的增加,但是其中一个事务发生唯一键冲突,如果能够回退主键值,你就会发现,原先没有数据的主键位置,已经有值了,继续插入又会报主键冲突

  • 解决主键冲突,这时在支持自增值回退的前提下
  • 第一个就是事先检查,这个太耗资源了不采取
  • 第二个把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。
  • 总结

可见在支持自增值回退的前提下会大大影响性能,所以干脆不支持回退。所以才只保证了自增 id 是递增的,但不保证是连续的。

4.自增锁的优化(第三种原因,主要就是自增id申请策略)

  • 概述

在5.0之前MySQL采用的是直到语句执行完才释放自增锁,而到了5.0之后则根据一个参数的值而定innodb_autoinc_lock_mode,默认为1

  1. 设置为0,采取5.0之前的策略
  2. 设置为1,分两种情况,普通insert拿了就释放;批量insert ... select、replace … select 和 load data 语句,是等语句执行完在释放
  3. 设置为2,所有的申请自增主键的动作都是申请后就释放锁。
  • 为什么采用默认时,对于批量的语句还是会等到执行完才释放

原因就是为了数据一致性,当不等执行完就释放,在高并发下的插入会出现我们表面意义上的不一致,但在逻辑意义上又是一致的,下面举个例子

  • 场景

如果SessionB不等insert…select执行完就释放,会出现下面的数据

  • session B 先插入了两个记录,(1,1,1)、(2,2,2);
  • 然后,session A 来申请自增 id 得到 id=3,插入了(3,5,5);
  • 之后,session B 继续执行,插入两条记录 (4,3,3)、 (5,4,4)。
    关于极客时间 | MySQL实战45讲的部分总结_第52张图片
  • 解释

虽然语义上没有要求id与c是一致的,但是由于binlog的存在,要么先记A,要么先记B,无论那种顺序在备库执行的时候都是按照递增的方式,也就是说最后就导致了数据不一致

  • 解决

一种就是等类似这种语句执行完在释放自增锁,
另一种就是在 binlog 里面把插入数据的操作都如实记录进来,到备库执行的时候,不再依赖于自增主键去生成。这种情况,其实就是 innodb_autoinc_lock_mode 设置为 2,同时 binlog_format 设置为 row。(生产环境下推荐这种)

  • 疑问

为什么普通insert就不用等执行完在释放呢,这是因为普通insert对于插入的记录数是已知的,拿到锁申请对应数量就好了,而批量则对于这个过程是未知的,那么需要一个申请一个呢?如果有10万个要进行10万次申请?

  • MySQL中的申请策略,在这种策略下,提前申请的值会被浪费,自然而然会出现不连贯
  • 语句执行过程中,第一次申请自增 id,会分配 1 个;
  • 1 个用完以后,这个语句第二次申请自增 id,会分配 2 个;
  • 2 个用完以后,还是这个语句,第三次申请自增 id,会分配 4 个;
  • 依此类推,同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍。

不是普通的insert锁为什么这么多

1.insert … select 语句

  • 场景搭建

对于这两个语句的执行,如果B先执行会对表t主键索引加上(-无穷,1]的这个区间加上next锁,所以A会等待B执行完,但如果B没有进行加锁,虽然B执行,但由于是批量的会导致A在其中某句之间添加成功,那么在binlog中就会显示出这个顺序,也就是说在备库在进行同步时,会把-1这个记录也加入到t2中
insert into t values(-1,-1,-1);
insert into t2(c,d) select c,d from t;
在这里插入图片描述

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;

insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);

create table t2 like t

2.insert循环写入

  • 情况一:对t2进行插入如下语句
  • 此句的加锁范围就是表 t 索引 c 上的 (3,4]和 (4,supremum]这两个 next-key lock,以及主键索引上 id=4 这一行。它的执行流程也比较简单,从表 t 中按照索引 c 倒序,扫描第一行,拿到结果写入到表 t2 中。因此整条语句的扫描行数是 1。通过慢查询日志会看到扫描语句就是1
insert into t2(c,d)  (select c+1, d from t force index(c) order by c desc limit 1);
  • 情况二:对t进行插入如下语句

此语句通过查看执行计划和日志发现扫描到了5行并且使用了临时表,

  • 创建临时表,表里有两个字段 c 和 d。默认临时表是使用 Memory 引擎的
  • 按照索引 c 扫描表 t,依次取 c=4、3、2、1,然后回表,读到 c 和 d 的值写入临时表。这时,Rows_examined=4。加锁范围变成了全部每个间隙都有
  • 由于语义里面有 limit 1,所以只取了临时表的第一行,再插入到表 t 中。这时,Rows_examined 的值加 1,变成了 5。
insert into t(c,d)  (select c+1, d from t force index(c) order by c desc limit 1);
  • 对情况二的解释
  • 为什么用到了临时表:原因是这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符。
  • 为什么遍历的全部:由于实现上这个语句没有在子查询中就直接使用 limit 1,从而导致了这个语句的执行需要遍历整个表 t。它的优化方法也比较简单,就是用前面介绍的方法,先 insert into 到临时表 temp_t,这样就只需要扫描一行;然后再从表 temp_t 里面取出这行数据插入表 t1。即第一种方式

3.insert唯一键冲突

  • 例子

session A 执行的 insert 语句,发生唯一键冲突的时候,并不只是简单地报错返回,还在冲突的索引上加了锁。我们前面说过,一个 next-key lock 就是由它右边界的值定义的。这时候,session A 持有索引 c 上的 (5,10]共享 next-key lock(读锁)。
关于极客时间 | MySQL实战45讲的部分总结_第53张图片

  • 死锁例子
  • 在 T1 时刻,启动 session A,并执行 insert 语句,此时在索引 c 的 c=5 上加了记录锁。注意,这个索引是唯一索引,因此退化为记录锁(如果你的印象模糊了,可以回顾下第 21 篇文章介绍的加锁规则)。
  • 在 T2 时刻,session B 要执行相同的 insert 语句,发现了唯一键冲突,加上读锁;同样地,session C 也在索引 c 上,c=5 这一个记录上,加了读锁。
  • T3 时刻,session A 回滚。这时候,session B 和 session C 都试图继续执行插入操作,都要加上写锁。两个 session 都要等待对方的行锁,所以就出现了死锁。关于极客时间 | MySQL实战45讲的部分总结_第54张图片
    关于极客时间 | MySQL实战45讲的部分总结_第55张图片

4.insert into … on duplicate key update

  • 概述

上面的例子是主键冲突了,改下成如下语句就会给索引 c 上 (5,10] 加一个排他的 next-key lock(写锁)。insert into … on duplicate key update 这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。

insert into t values(11,10,10) on duplicate key update d=100; 

关于极客时间 | MySQL实战45讲的部分总结_第56张图片

如何快速的复制一张表

  • 前言

上面我们说到对于一张表我们需要其中的几列几行数据使用insert…select来插入就可,不过最后我们提到这个会给读取到的数据加读锁。当然,为了避免对源表加读锁,更稳妥的方案是先将数据写到外部文本文件,然后再写回目标表。有两种可行性方案

1.mysqldump方法

  • 导出
mysqldump -h$host -P$port -u$user --add-locks=0 --no-create-info --single-transaction  --set-gtid-purged=OFF db1 t --where="a>900" --result-file=/client_tmp/t.sql
  • –single-transaction 的作用是,在导出数据的时候不需要对表 db1.t 加表锁,而是使用 START TRANSACTION WITH CONSISTENT SNAPSHOT 的方法;
  • –add-locks 设置为 0,表示在输出的文件结果里,不增加" LOCK TABLES t WRITE;" ;
  • –no-create-info 的意思是,不需要导出表结构;
  • –set-gtid-purged=off 表示的是,不输出跟 GTID 相关的信息;
  • –result-file 指定了输出文件的路径,其中 client 表示生成的文件是在客户端机器上的。
    关于极客时间 | MySQL实战45讲的部分总结_第57张图片
  • 导出
mysql -h127.0.0.1 -P13000  -uroot db2 -e "source /client_tmp/t.sql"

2.导出CSV文件

  • 导出语句
select * from db1.t where a>900 into outfile '/server_tmp/t.csv';

注意事项:

  • 这条语句会将结果保存在服务端。如果你执行命令的客户端和 MySQL 服务端不在同一个机器上,客户端机器的临时目录下是不会生成 t.csv 文件的。
  • into outfile 指定了文件的生成位置(/server_tmp/),这个位置必须受参数 secure_file_priv 的限制。参数 secure_file_priv 的可选值和作用分别是:
    —如果设置为 empty,表示不限制文件生成的位置,这是不安全的设置;
    —如果设置为一个表示路径的字符串,就要求生成的文件只能放在这个指定的目录,或者它的子目录;
    —如果设置为 NULL,就表示禁止在这个 MySQL 实例上执行 select … into outfile 操作。
  • 这条命令不会帮你覆盖文件,因此你需要确保 /server_tmp/t.csv 这个文件不存在,否则执行语句时就会因为有同名文件的存在而报错。
  • 这条命令生成的文本文件中,原则上一个数据行对应文本文件的一行。但是,如果字段中包含换行符,在生成的文本中也会有换行符。不过类似换行符、制表符这类符号,前面都会跟上“\”这个转义符,这样就可以跟字段之间、数据行之间的分隔符区分开。
  • 导出语句
load data infile '/server_tmp/t.csv' into table db2.t;

语句执行流程

  • 打开文件 /server_tmp/t.csv,以制表符 (\t) 作为字段间的分隔符,以换行符(\n)作为记录之间的分隔符,进行数据读取;
  • 启动事务。
  • 判断每一行的字段数与表 db2.t 是否相同:
    -----若不相同,则直接报错,事务回滚;
    -----若相同,则构造成一行,调用 InnoDB 引擎接口,写入到表中。
  • 重复步骤 3,直到 /server_tmp/t.csv 整个文件读入完成,提交事务。
  • CSV主备同步流程
    关于极客时间 | MySQL实战45讲的部分总结_第58张图片

grant之后要跟着flush privileges吗?

  • grant命令的概述

简单的来说这个命令就是更改操作实体的各类权限

  • 创建一个用户

这条语句的逻辑是创建一个用户’ua’@’%’,密码是 pa。注意,在 MySQL 里面,用户名 (user)+ 地址 (host) 才表示一个用户,因此 ua@ip1 和 ua@ip2 代表的是两个不同的用户。

# 这里的百分号为止,就是填写ip的位置,填成%就是全部IP都管用
create user 'ua'@'%' identified by 'pa';

上面这个命令在数据库内做了两件事

  • 磁盘上,往 mysql.user 表里插入一行,由于没有指定权限,所以这行数据上所有表示权限的字段的值都是 N;
  • 内存里,往数组 acl_users 里插入一个 acl_user 对象,这个对象的 access 字段值为 0。

1.全局权限

  • 语句做的事情
  • 磁盘上,将 mysql.user 表里,用户’ua’@’%'这一行的所有表示权限的字段的值都修改为‘Y’;
  • 内存里,从数组 acl_users 中找到这个用户对应的对象,将 access 值(权限位)修改为二进制的“全 1”。
# 像这种写法万万要不得,一般在生产环境上要合理控制用户权限的范围
grant all privileges on *.* to 'ua'@'%' with grant option;
  • 注意
  • grant 命令对于全局权限,同时更新了磁盘和内存。命令完成后即时生效,接下来新创建的连接会使用新的权限。
  • 对于一个已经存在的连接,它的全局权限不受 grant 命令的影响。
  • 回收权力
  1. 磁盘上,将 mysql.user 表里,用户’ua’@’%'这一行的所有表示权限的字段的值都修改为“N”;
  2. 内存里,从数组 acl_users 中找到这个用户对应的对象,将 access 的值修改为 0。
revoke all privileges on *.* from 'ua'@'%';

2.库(db)权限

  • 赋权语句
  • 磁盘上,往 mysql.db 表中插入了一行记录,所有权限位字段设置为“Y”;
  • 内存里,增加一个对象到数组 acl_dbs 中,这个对象的权限位为“全 1”。
grant all privileges on db1.* to 'ua'@'%' with grant option;

  • 全局与库权限的区别

db权限修改之后对已经存在的链接会立即生效,而全局是新的链接才会生效。原因就是:全局的权限,线程会保存之前的权限直到释放,db权限是每一次操作都要去内存表里面读取判断一下,读取的是全局对象

3.表权限和列权限

  • 赋权代码
create table db1.t1(id int, a int);

grant all privileges on db1.t1 to 'ua'@'%' with grant option;
GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
  • 生效

都是修改完即可生效,因为去修改权限表了。也会马上影响到已经存在的连接。

4.使用flush privileges之前的总结

  • 概述

看到这里,你一定会问,看来 grant 语句都是即时生效的,那这么看应该就不需要执行 flush privileges 语句了呀。答案也确实是这样的。

flush privileges 命令会清空 acl_users 数组,然后从 mysql.user 表中读取数据重新加载,重新构造一个 acl_users 数组。也就是说,以数据表中的数据为准,会将全局权限内存数组重新加载一遍。

同样地,对于 db 权限、表权限和列权限,MySQL 也做了这样的处理。

也就是说,如果内存的权限数据和磁盘数据表相同的话,不需要执行 flush privileges。而如果我们都是用 grant/revoke 语句来执行的话,内存和数据表本来就是保持同步更新的。

5.flush privileges 使用场景

  • 概述

那么根据上面的小结,需要flush这个命令的时候就是内存中的权限表和磁盘中不一致了我就需要在赋权命令后加上这个。而这种不一致通常是操作不规范所造成的,我们下面来看两个诡异的不规范

  • 例子一

可以看到,T3 时刻虽然已经用 delete 语句删除了用户 ua,但是在 T4 时刻,仍然可以用 ua 连接成功。原因就是,这时候内存中 acl_users 数组中还有这个用户,因此系统判断时认为用户还正常存在。在 T5 时刻执行过 flush 命令后,内存更新,T6 时刻再要用 ua 来登录的话,就会报错“无法访问”了。关于极客时间 | MySQL实战45讲的部分总结_第59张图片

  • 例子二

可以看到,由于在 T3 时刻直接删除了数据表的记录,而内存的数据还存在。这就导致了:T4 时刻给用户 ua 赋权限失败,因为 mysql.user 表中找不到这行记录;而 T5 时刻要重新创建这个用户也不行,因为在做内存判断的时候,会认为这个用户还存在。
关于极客时间 | MySQL实战45讲的部分总结_第60张图片

要不要使用分区表

1.分区表是什么

创建如下的分区表,在引擎层来说这时四个表,对于server层来说这时一个表

CREATE TABLE `t` (
  `ftime` datetime NOT NULL,
  `c` int(11) DEFAULT NULL,
  KEY (`ftime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB,
 PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB,
 PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB,
PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB);

# 按照约定这两条记录分别落入了2018、2019这两个分区
insert into t values('2017-4-1',1),('2018-4-1',1);

在这里插入图片描述

1.1 InnoDB分区表的引擎层的行为

关于极客时间 | MySQL实战45讲的部分总结_第61张图片

  • 对于普通表这个插入语句的间隙锁

也就是说,‘2017-4-1’ 和’2018-4-1’ 这两个记录之间的间隙是会被锁住的。那么,sesion B 的两条插入语句应该都要进入锁等待状态。
关于极客时间 | MySQL实战45讲的部分总结_第62张图片

  • 对于分区表这个插入语句间隙锁

从上面的实验效果可以看出,session B 的第一个 insert 语句是可以执行成功的。这是因为,对于引擎来说,p_2018 和 p_2019 是两个不同的表,也就是说 2017-4-1 的下一个记录并不是 2018-4-1,而是 p_2018 分区的 supremum。所以 T1 时刻,在表 t 的 ftime 索引上,所以,session B 要写入一行 ftime 是 2018-2-1 的时候是可以成功的,而要写入 2017-12-1 这个记录,就要等 session A 的间隙锁。
关于极客时间 | MySQL实战45讲的部分总结_第63张图片

1.2 引擎是MyISAM会发生什么

对于MyISAM这个引擎来说,当执行该update语句由于只支持表锁,所以这个语句会锁住这个表中的所有读,但是B的第一句执行成功但第二句缺执行失败
关于极客时间 | MySQL实战45讲的部分总结_第64张图片

  • 原因

这正是因为 MyISAM 的表锁是在引擎层实现的,session A 加的表锁,其实是锁在分区 p_2018 上。因此,只会堵住在这个分区上执行的查询,落到其他分区的查询是不受影响的。

2.手动分表与分区表的区别

  • 概述

上面我们说了挺多好处的,但是为什么不让用分区表呢,其中一个原因就是单个表的体积过于大,

比如,按照年份来划分,我们就分别创建普通表t_2017、t_2018、t_2019等等。手工分表的逻辑,也是找到需要更新的所有分表,然后依次执行更新。在性能上,这和分区表并没有实质的差别。分区表和手工分表,一个是由 server 层来决定使用哪个分区,一个是由应用层代码来决定使用哪个分表。因此,从引擎层看,这两种方式也是没有差别的。其实这两个方案的区别,主要是在 server 层上。从 server 层看,我们就不得不提到分区表一个被广为诟病的问题:打开表的行为。

2.1 分区策略

  • 概述

每当第一次访问一个分区表的时候,MySQL 需要把所有的分区都访问一遍。一个典型的报错情况是这样的:如果一个分区表的分区很多,比如超过了 1000 个,而 MySQL 启动的时候,open_files_limit 参数使用的是默认值 1024,那么就会在访问这个表的时候,由于需要打开所有的文件,导致打开表文件的个数超过了上限而报错。

  • MyISAM分区策略

分区表使用的分区策略,我们称为通用分区策略(generic partitioning),每次访问分区都由 server 层控制。通用分区策略,是 MySQL 一开始支持分区表的时候就存在的代码,在文件管理、表管理的实现上很粗糙,因此有比较严重的性能问题。即上面的报错就是基于MyISAM引擎报的错

  • InnoDB的分区策略

InnoDB 引擎引入了本地分区策略(native partitioning)。这个策略是在 InnoDB 内部自己管理打开分区的行为。并且在5.7.17后就不推荐使用MyISAM;而从8.0开始直接就不允许建立MyISAM的分区表了

2.2 分区表的 server 层行为

  • 概述

在server层只看做一个。表可以看到,虽然 session B 只需要操作 p_2017 这个分区,但是由于 session A 持有整个表 t 的 MDL 锁,就导致了 session B 的 alter 语句被堵住。
关于极客时间 | MySQL实战45讲的部分总结_第65张图片

3.小结

  1. MySQL 在第一次打开分区表的时候,需要访问所有的分区;
  2. 在 server 层,认为这是同一张表,因此所有分区共用同一个 MDL 锁;
  3. 在引擎层,认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问必要的分区。

3.1 分区表的应用场景

分区表的一个显而易见的优势是对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁。还有,分区表可以很方便的清理历史数据。
如果一项业务跑的时间足够长,往往就会有根据时间删除历史数据的需求。这时候,按照时间分区的分区表,就可以直接通过 alter table t drop partition … 这个语法删掉分区,从而删掉过期的历史数据。
这个 alter table t drop partition … 操作是直接删除分区文件,效果跟 drop 普通表类似。与使用 delete 语句删除数据相比,优势是速度快、对系统影响小。

关于join的补充、NLJ和BNL的补充、group by和distinct的补充

!!!!

自增ID用完了怎么办

  • 对于表定义的自增 ID :

用完了之后会取得相同的 ID,然后在插入的时候抛出异常;

  • 对于系统自增的 row_id

对于 InnoDB 引擎,如果一个表没有制定主键,那么, 那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id;所以范围为 0到2^48-1,当达到上限之后会又变成 0,然后重新循环,所以新插入的记录会覆盖之前的记录;

  • 对于 Xid
  • Xid 是redo log 和 binlog 的共同字段,来标识一个事务;
  • Xid 由MySQL 内部维护的一个全局变量 global_query_id 来生成,基于内存生成所以在 MySQL 重启的时候会消失,但是重启之后也会生成新的 binlog 所以只需要保证在一个 binlog 中没有重复的 Xid 即可;
  • 对于 InnoDB 的 trx_id :
  • InnoDB 的 trx_id 也就是 MVCC 实现使用的事务 ID;
  • 该 ID 是会持久化的,所以当超过 ID 最大值之后,就会重新生成 0 的 trx_id,而此时,因为 MVCC 的实现,会导致该数据可见造成脏读(我认为是幻读);
  • 对于 thread_id
  • 对于 MySQL 实例,每新建一个连接,就会生成一个对应的 thread_id,通过内存中一个变量来进行递增;
  • 会将所有 thread_id 保存在数组中,当达到上限,就会一直循环寻找已经被删除的 thread_id 也就是释放连接的现场,复用 thread_id;

你可能感兴趣的:(mysql,mysql,sql)