SQL语句在MySQL中是如何执行的(二)

参考:

https://mp.weixin.qq.com/s?__biz=MzU3NDkwMjAyOQ==&mid=2247483903&idx=1&sn=bfe075f5121a2335efb7742fdba9b6fc&chksm=fd2a1b9dca5d928be00ede2ee959e9c0359844383781485556448969b74c7dc111f246e6f967&scene=178&cur_album_id=1552870102100983810#rd

http://www.cppcns.com/shujuku/mysql/303692.html

 

 

 

 

update语句到 redo log深入理解

前面我们分析过一个查询语句的执行流程,并且解释了执行过程中涉及的模块。一条查询语句一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。老铁们可以点击链接 查询语句在 MySQL 如何执行 学习上篇内容。

这次,我们来深入学习一条更新语句在 MySQL 中的执行流程。通过此文我们可以充分了解 什么是 Redo Log

表结构创建

首先我们先创建一张表,只有主键 ID,以及 int 类型字段 c。

create table T(ID int primary key, c int);

现在我们要更新一条数据,语句如下:

update T set c=c+1 where ID=2;

更新语句其实也跟查询语句的流程类似,只不过多了 redo log、undo log 以及 binlog 日志

上一篇查询语句的执行流程我们说过,在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条语句会把整个 T 表的缓存结果都清空。这也是为何我们不建议使用查询缓存的原因。

账本与记账板

假如您当了小超市老板,自然会有一个账本记录交易记录,但是可能还要一个赊账记录。因为村里有个姑娘叫小芳,长得美丽又善良。有时候会到你这里白嫖,额,不是,是赊账。你先把记录写在小粉板上,等夜深人静的时候就把粉板的数据同步到归档的账本中。当然粉板也有满的时候,所以当粉板满了就要对账写入账本中,

所以,如果有人要来赊账,或者还账的时候,通常有两种做法:

  1. 直接把账本翻出来,把这次的赊账加上去或者扣除。
  2. 先在粉板上记下这次的帐,等打烊后再把账本翻出来核算。

在生意忙的时候,我们肯定选择后者,因为前者操作太麻烦了。首先,你得找到这个人的赊账总额那条记录。你想想,密密麻麻几十页,掌柜要找到那个名字,可 能还得带上老花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上。这个时候小芳来赊账,等半天。以后还怎么约小芳到小树林呢?

在 MySQL 中也有这个问题,如果每一次操作都要写进磁盘,然后磁盘也要找到对应的记录,然后再更新。整个过程的 IO 成本,查询成本都很高,为了解决这个问题,MySQL的设计者就用了类似小超市老板粉板的思路来提升更新效率。

而粉板和账本配合的整个过程,其实就是 MySQL 里经常说到的 WAL 技术,WAL 的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本

redo log

首先我们要明确的是binlog 日志是在 server 层的,而redo log是 InnoDB 特有的。

当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到 redo log(粉板)中,并更新内存,这个时候就算完成了。同时 引擎会在适当的时候将这个记录更新到磁盘里,而更新往往是系统比较闲的时候,这就是打样以后掌柜做的事情。

类似的,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件

write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe

要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。

binlog

MySQL 的整体架构其实有两块:一块是 Server 层,还有一块是 引擎层,负责存储相关。前面我们提到的 redo log 是InnoDB 引擎持有的,而 Server 层也有自己的日志,叫 binlog(归档日志)。

那为何会有两份日志呢?

因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力(因为是 Server 层与引擎层是两个独立的模块),binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。

假如只有 binlog,当 Server 层 binlog 日志写完后 引擎层还没有同步到磁盘就断电了。这个时候重启后 binlog 记录了更新操作,但是引擎层并没有写入磁盘中就导致了从库使用该 binlog 同步数据不一致

redo log、 binlog 的 差异

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
  2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

Update 语句执行流程

有了对两个日志的概念理解,我们就可以继续理解执行器与 InnoDB 引擎执行 update 语句时的内部流程。

  1. Server 层的执行器先调用引擎取出 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到数据把这个值 + 1,得到新一行的数据,再调用存储引擎接口写入这行新数据。
  3. InnoDB 引擎将这行数据更新到内存中,同时将这个更新操作所影响的页日志记录到 redo log 中,此时日志处于 prepare 状态,然后会告知 执行器完成了,随时可以提交事务。
  4. 执行器生成这个操作的 binlog ,并把 binlog 写入磁盘。
  5. 执行器继续调用引擎的的提交事务接口,引擎收到请求就把刚刚写入的 redo log 的状态改成提交(commit),更新完成。

最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。

如下图所示,绿色代表执行器执行,白色代表 InnoDB 引擎执行:

 

两阶段提交

为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。要说明这个问题,我们得从文章开头的那个问题说起:怎样让数据库恢复到半个月内任意一秒的状态?

前面我们说过了,binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。

当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:

  1. 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
  2. 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。

由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。(会造成数据不一致)

仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?

先写 redo log 后写 binlog

假如在引擎 写完 redo log 后,bin log 没有写完,异常重启,依然可以根据 redo log 日志把数据恢复,但是 binlog 没有记录这个语句。所以从库 通过 binlog 同步数据就导致没有把这个这行数据同步过来,丢失了这个事务操作造成数据不一致。

先写 binlog 再写 redo log

如果写完 binlog 后 崩溃,由于 redo log 还没有写,崩溃恢复后这个事务无效,但是 binlog 却有记录。从库根据 这个 binlog 日志就会导致多处一个事务,与主库不一致。

简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。(敲黑板了同学们)

 

 

 

 

 

 

mysql update语句的执行过程详解

以前有过一篇关于MySQL查询语句的执行过程,这里总结一下update语句的执行过程。由于update涉及到数据的修改,所以,很容易推断,update语句比select语句会更复杂一些。

mysql update语句的执行过程详解

1,准备

创建一张test表

  1. CREATE TABLE `test` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `c` int(11) NOT NULL DEFAULT '0' COMMENT '数值',
  4. PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='测试表';

插入三条数据

  1. INSERT INTO `test` (`c`) VALUES (1), (2), (3);

2,测试

加入我要把第一条数据的 c 值 加 1,则

  1. UPDATE `test` SET `c` = `c` + 1 WHERE `id` = 1;

按照我们平常的思路,就是找出这条记录,把它的值改好,保存就OK了。

但我们追究一下细节,由于涉及到修改数据,所以涉及到日志了。

3 操作顺序

3.1 查找记录:执行器先找引擎取id=1这一行。ID是主键,引擎直接用树搜索找到这一行。如果id=1这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回;

3.2 执行器拿到引擎返回的行数据,把num改为2,得到新的一行数据,再调用引擎接口写入这行新数据;

3.3 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态;

3.4 引擎告知执行器,我执行完成了,你随时可以调我的接口提交事务了;

3.5 执行器生成这个操作的binlog,并把binlog写入磁盘。

3.6 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交commit状态,更新完成。

mysql update语句的执行过程详解

binlog是MySQL内部实现二阶段提交协调者,它为每个事务分配一个事务ID: XID
一阶段
开启事务,redo log 和 undo log已经记录了对应的日志,此时事务状态为prepare
二阶段

binlog 完成write和fsync后,成功,事务一定提交了,否则事务回滚 发送commit,清除undo信息,刷redo,设置事务状态为completed
  4, 两种日志

4.1 重做日志 redo log

redo log 通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。

一般更新会有如下做法:

直接查询原始数据,立马更新; 先找个临时记事本,做下记录,等不忙的时候/结算时候进行核算更新。

第一种做法在高并发IO的情况下非常的不容乐观。所以一般都会采用第二种方式。

同样在MySQL里存在这样的一个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本、查找成本都很高。为了解决这个问题,MySQL的设计者就用了redo log的思路来提升更新效率。

临时记事本和原始数据进行操作的整个过程,也是对应MySQL里经常说到的WAL(预写式日志)技术WAL的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log(记事本)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个记事本里的操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。

问题:redo log(记事本)写满如何解决?
InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这块“记事本”总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

write pos和checkpoint之间的是“记事本”上还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。

有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe

4.2 归档日志 binlog

MySQL整体来看,其实就有两块:一块是Server层,它主要做的是MySQL功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面redo log是InnoDB引擎特有的日志,而Server层也有自己的日志,称为binlog(归档日志)。

为什么会有两份日志呢?

最开始MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。

这两种日志有以下三点不同。

redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。

redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。

redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

5 两阶段提交

将redo log的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。

5.1 为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。

binlog会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的DBA承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。

当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:

首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;

然后,从备份的时间点开始,将备份的binlog依次取出来,重放到中午误删表之前的那个时刻。

这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。

说完了数据恢复过程,我们回来说说,为什么日志需要“两阶段提交”。这里不妨用反证法来进行解释。

5.2 如果不用两阶段提交

由于redo log和binlog是两个独立的逻辑,如果不用两阶段提交,要么就是先写完redo log再写binlog,或者采用反过来的顺序。

我们看看这两种方式会有什么问题。

仍然用前面的update语句来做例子,当前ID=1的行,字段c的值是1。

5.2.1 先写redo log,后写binlog

假设在redo log写完了,但binlog还没有写完的时候,MySQL进程异常重启。由于我们前面说过的,redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是2。但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句,如果需要用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次更新,恢复出来的这一行c的值就是1,与原库的值不同。

5.2.2 先写binlog,后写redo log

如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,所以这一行c的值是1。但是binlog里面已经记录了“把c从1改成2”这个日志。所以,在之后用binlog来恢复的时候,恢复出来的临时库中的这一行c值就是2,与原库的值不同。

可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

其实不只是误操作后需要用这个过程来恢复数据。当需要扩容的时候,也就是需要再多搭建一些备库来增加系统的读能力的时候,现在常见的做法也是用全量备份加上应用binlog来实现的,这个“不一致”就会导致你的线上出现主从数据库不一致的情况。

简单说,redo log和binlog都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。

redo log 用于保证crash-safe能力。innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数建议设置成1,这样可以保证MySQL异常重启之后数据不丢失。

sync_binlog 这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。这个参数也建议设置成1,这样可以保证MySQL异常重启之后binlog不丢失。

 

 

 

 

 

 

一条更新SQL执行流程(Buffer Pool、Change Buffer、Adaptive Hash Index、Redo Log)

1、一个查询语句SQL是如何执行的?

 
SQL执行过程

1.1、 通信协议

MySQL 是支持多种通信协议的,可以使用同步/异步的通讯类型,支持长连接/短连接。

  • a、通信类型:同步 or 者异步

    • 同步通信的特点:
      • 1、同步通信依赖于被调用方,受限于被调用方的性能。应用服务器操作数据库,线程会阻塞,等待数据库的返回。(常用的方式)
      • 2、一般只能做到一对一,很难做到一对多的通信。
    • 异步通讯的特点:
      • 1、异步可以避免应用阻塞等待,但是不能节省 SQL 执行的时间。
      • 2、如果异步存在并发,每一个 SQL 的执行都要单独建立一个连接,避免数据混乱。 但是这样会给服务端带来巨大的压力(一个连接就会创建一个线程,线程间切换会占用 大量 CPU 资源)。另外异步通信还带来了编码的复杂度,所以一般不建议使用。如果要异步,必须使用连接池,排队从连接池获取连接而不是创建新连接。

    一般来说连接数据库都是同步连接。

  • b、连接方式:
    MySQL 既支持短连接,也支持长连接。

    • 短连接就是操作完毕以后,马上 close 掉。
    • 长连接可以保持打开,减少服务端创建和释放连接的消耗,后面的程序访问的时候还可以使用这个连接。默认会在连接池中使用长连接

    保持长连接会消耗内存。长时间不活动的连接,MySQL 服务器会断开。(默认都是 28800 秒,8 小时)

## 查看交互超时时间
show global variables like 'wait_timeout'; -- 非交互式超时时间,如 JDBC 程序
show global variables like 'interactive_timeout'; -- 交互式超时时间,如数据库工具
## 查看 MySQL 当前有多少个连接?
show global status like 'Thread%';
##Threads_cached:缓存中的线程连接数。
##Threads_connected:当前打开的连接数。
##Threads_created:为处理连接创建的线程数。 
##Threads_running:非睡眠状态的连接数,通常指并发连接数。
【思考🤔:MySQL 服务允许的最大连接数是多少呢?】

【答案】: 在 5.7 版本中默认是 151 个,最大可以设置成 16384(2^14)。

  • c、通信协议:


     
    通信协议
    • 1、单工:
      在两台计算机通信的时候,数据的传输是单向的。
      生活中的类比:遥控器。
    • 2、半双工:
      在两台计算机之间,数据传输是双向的,你可以给我发送,我也可以给你发送,
      但是在这个通讯连接里面,同一时间只能有一台服务器在发送数据,也就是你要给我发的话,也必须等我发给你完了之后才能给我发。
      生活中的类比:对讲机。
    • 3、全双工:
      数据的传输是双向的,并且可以同时传输。
      生活中的类比:打电话。
【思考🤔:MySQL 使用了半双工的通信方式?】

【答案】:要么是客户端向服务端发送数据,要么是服务端向客户端发送数据,这两个动作不能同时发生。所以客户端发送 SQL 语句给服务端的时候,(在一次连接里面)数据是不能分成小块发送的,不管你的 SQL 语句有多大,都是一次性发送。
比如用 MyBatis 动态 SQL 生成了一个批量插入的语句,插入 10 万条数据,values 后面跟了一长串的内容,或者 where 条件 in 里面的值太多,会出现问题。这个时候必须要调整 MySQL 服务器配置 max_allowed_packet 参数的值(默认 是 4M),把它调大,否则就会报错。
另一方面,对于MySql服务端来说,也是一次性发送所有的数据,不能因为你已经取到了想要的数据就中断操作,这个时候会对网络和内存产生大量消耗。
所以,一定要在程序里面避免不带 limit 的这种操作,比如一次把所有满足条件 的数据全部查出来,一定要先 count 一下。如果数据量的话,可以分批查询。

1.2.、查询缓存(Query Cache):

MySQL 内部自带了一个缓存模块。
缓存的作用:把数据以 KV 的形式放到内存里面,可以加快数据的读取速度,也可以减少服务器处理的时间。但是 MySQL 的缓存比较陌生,从来没有去配置过,也不知道它什么时候生效?
MySQL 的缓存默认是关闭的。
【思考🤔问题:默认关闭的意思就是不推荐使用,为什么 MySQL 不推荐使用它自带的缓存呢?】
【答案】:**主要是因为 MySQL 自带的缓存的应用场景有限:

  • 第一个是它要求 SQL 语句必须一 模一样,中间多一个空格,字母大小写不同都被认为是不同的的 SQL。
  • 第二个是表里面任何一条数据发生变化的时候,这张表所有缓存都会失效,所以对于有大量数据更新的应用,也不适合。
    所以缓存这一块,还是交给 ORM 框架(比如 MyBatis 默认开启了一级缓存), 或者独立的缓存服务,比如 Redis 来处理更合适。

在 MySQL 8.0 中,查询缓存已经被移除了。

1.3、语法解析和预处理(Parser & Preprocessor):

  • 1.3.1、词法解析
    词法分析就是把一个完整的 SQL 语句打碎成一个个的单词。
  • 1.3.2、语法解析
    语法分析会对 SQL 做一些语法检查,比如单引号有没有闭合, 然后根据 MySQL 定义的语法规则,根据 SQL 语句生成一个数据结构。这个数据结构我 们把它叫做解析树(select_lex)。
     
    解析树
  • 1.3.3、预处理器

【思考🤔】:如果写了一个词法和语法都正确的 SQL,但是表名或者字段不存在,会在哪里报错? 是在数据库的执行层还是解析器?

select * from balabala;

【答案】:解析器可以分析语法,但是它怎么知道数据库里面有什么表,表里面有什么字段呢? 实际上还是在解析的时候报错,解析 SQL 的环节里面有个预处理器。 它会检查生成的解析树,解决解析器无法解析的语义。比如,它会检查表和列名是
否存在,检查名字和别名,保证没有歧义。 预处理之后得到一个新的解析树。

1.4、查询优化(Query Optimizer)与查询执行计划:

  • 1.4.1、什么是优化器?
    查询优化器的目的:就是根据解析树生成不同的执行计划(Execution Plan),然后选择一种最优的执行计划,MySQL 里面使用的是基于开销(cost)的优化器,哪种执行计划开销最小,就用哪种。
  • 1.4.2、优化器可以做什么?
    MySQL 的优化器能处理哪些优化类型呢? 举两个简单的例子:
    1、对多张表进行关联查询的时候,以哪个表的数据作为基准表。
    2、有多个索引可以使用的时候,选择哪个索引。 实际上,对于每一种数据库来说,优化器的模块都是必不可少的,他们通过复杂的算法实现尽可能优化查询效率的目标。 如果对于优化器的细节感兴趣,可以看看《数据库查询优化器的艺术-原理解析与 SQL性能优化》。
  • 1.4.3.优化器是怎么得到执行计划的?
    首先要启用优化器的追踪(默认是关闭的):
    注意!!!开启这开关是会消耗性能,因为它要把优化分析的结果写到表里面,所以不要轻易开启,或者查看完之后关闭它(改成 off)。
    注意:参数分为 session 和 global 级别。 接着执行一个 SQL 语句,优化器会生成执行计划:
select t.tcid from teacher t,teacher_contact tc where t.tcid = tc.tcid; 

这个时候优化器分析的过程已经记录到系统表里面了,可以查询:

select * from information_schema.optimizer_trace\G

分析完记得关掉它:

set optimizer_trace="enabled=off";
SHOW VARIABLES LIKE 'optimizer_trace';
  • 1.4.4. 优化器得到的结果

【思考🤔问题】:优化完之后,得到一个什么东西呢?

【答案】:优化器最终会把解析树变成一个查询执行计划,查询执行计划是一个数据结构。 这个执行计划是不是一定是最优的执行计划呢? 不一定,因为 MySQL 也有可能覆盖不到所有的执行计划。
怎么查看 MySQL 的执行计划呢?
比如多张表关联查询,先查询哪张表?在执行查询的时候可能用到哪些索引,实际上用到了什么索引?
MySQL 提供了一个执行计划的工具。在 SQL 语句前面加上 EXPLAIN,就可以看到执行计划的信息。

EXPLAIN select name from user where id=1;

Explain 的结果也不一定最终执行的方式。

1.5、存储引擎:

【思考🤔】得到执行计划以后,SQL 语句是不是终于可以执行了?问题又来了:1、从逻辑的角度来说,数据是放在哪里的,或者说放在一个什么结构里面? 2、执行计划在哪里执行? 是谁去执行?

接下来时点给与解答:

  • 1.5.1、存储引擎基本介绍
    关系型数据库里面,数据是放在什么结构里面的? (放在表 Table 里面的)。表在存储数据的同时,还要组织数据的存储结构,这个存储结构就是由存储引擎决定的,所以也可以把存储引擎叫做表类型
    在 MySQL 里面,支持多种存储引擎,他们是可以替换的,所以叫做插件式的存储引擎。
  • 1.5.2、查看存储引擎
    数据库里面已经存在的表,怎么查看它们的存储引擎呢?
    通过sql or DDL 建表语句来查看。
show table status from `student`;

在 MySQL 里面,创建的每一张表都可以指定它的存储引擎,而不是一个数据库 只能使用一个存储引擎。存储引擎的使用是以表为单位的。而且,创建表之后还可以修改存储引擎。
一张表使用的存储引擎决定存储数据的结构,那在服务器上它们是怎么存储的呢?
首先要找到数据库存放数据的路径:

show variables like 'datadir';
  • 1.5.3、存储引擎比较
    MyISAM 和 InnoDB 是用得最多的两个存储引擎,在 MySQL 5.5 版本之前,默认的存储引擎是 MyISAM,它是 MySQL 自带的。创建表的时候不指定存储引擎, 它就会使用 MyISAM 作为存储引擎。
    MyISAM 的前身是 ISAM(Indexed Sequential Access Method:利用索引,顺序 存取数据的方法)。
    5.5 版本之后默认的存储引擎改成了 InnoDB,它是第三方公司为 MySQL 开发的。 为什么要改呢?最主要的原因还是 InnoDB 支持事务,支持行级别的锁,对于业务一致性要求高的场景来说更适合。
    这个里面又有 Oracle 和 MySQL 公司的一段恩怨情仇:
    InnoDB 本来是 InnobaseOy 公司开发的,它和 MySQL AB 公司合作开源了 InnoDB 的代码。但是没想到 MySQL 的竞争对手 Oracle 把 InnobaseOy 收购了。后来 08 年 Sun 公司(开发 Java 语言的 Sun)收购了 MySQL AB,09 年 Sun 公司 又被 Oracle 收购了,所以 MySQL,InnoDB 又是一家了。有人觉得 MySQL 越来越像 Oracle,其实也是这个原因。

     
    Oracle与Mysql恩怨情仇

     

  • 1.5.4、如何选择存储引擎?

    • 如果对数据一致性要求比较高,需要事务支持,可以选择 InnoDB。
    • 如果数据查询多更新少,对查询性能要求比较高,可以选择 MyISAM。
    • 如果需要一个用于查询的临时表,可以选择 Memory。
    • 如果所有的存储引擎都不能满足你的需求,并且技术能力足够,可以根据官网内部手册用 C 语言开发一个存储引擎😂。

1.6、执行引擎(Query Execution Engine)

【思考🤔】:是谁使用执行计划去操作存储引擎呢?

【答案】:这就是执行引擎,它利用存储引擎提供的相应的 API 来完成操作。
为什么修改了表的存储引擎,操作方式不需要做任何改变?
因为不同功能的存储引擎实现的 API 是相同的。
最后把数据返回给客户端,即使没有结果也要返回。

2、MySQL体系结构总结

2.1. 模块详解

 
mysql体系结构
  • Connector:用来支持各种语言和 SQL 的交互,比如 PHP,Python,Java 的 JDBC;
  • Management Serveices & Utilities:系统管理和控制工具,包括备份恢复、MySQL 复制、集群等等。
  • Connection Pool:连接池,管理需要缓冲的资源,包括用户密码权限线程等。
  • SQL Interface:用来接收用户的 SQL 命令,返回用户需要的查询结果
  • Parser:用来解析 SQL 语句。
  • Optimizer:查询优化器。
  • Cache and Buffer:查询缓存,除了行记录的缓存之外,还有表缓存,Key 缓
    存,权限缓存等等。
  • Pluggable Storage Engines:插件式存储引擎,它提供 API 给服务层使用,
    跟具体的文件打交道。

2.2. 架构分层

总体上,把 MySQL 分成三层,跟客户端对接的连接层,真正执行操作的服务层,和跟硬件打交道的存储引擎层(参考 MyBatis:接口、核心、基础)。


 
架构分层
  • 2.2.1、连接层
    客户端要连接到 MySQL 服务器 3306 端口,必须要跟服务端建立连接,那么管理所有的连接,验证客户端的身份和权限,这些功能就在连接层完成。
  • 2.1.2、服务层
    连接层会把 SQL 语句交给服务层,这里面又包含一系列的流程:比如查询缓存的判断、根据 SQL 调用相应的接口,对 SQL 语句进行词法和语法的解析(比如关键字怎么识别,别名怎么识别,语法有没有错误等等)。
    然后就是优化器,MySQL 底层会根据一定的规则对 SQL 语句进行优化,最后再交给执行器去执行。
  • 2.1.3.存储引擎
    存储引擎就是数据真正存放的地方,在 MySQL 里面支持不同的存储引擎。 再往下就是内存或者磁盘。

3、一条更新SQL是如何执行的?

3.1. 缓冲池 Buffer Pool

首先,InnnoDB 的数据都是放在磁盘上的,InnoDB 操作数据有一个最小的逻辑单位,叫做页(索引页和数据页)。对于数据的操作,不是每次都直接操作磁盘,因为磁盘的速度太慢了。InnoDB 使用了一种缓冲池的技术,也就是把磁盘读到的页放到一块内存区域里面。这个内存区域就叫 Buffer Pool。

 
buffer pool交互图

下一次读取相同的页,先判断是不是在缓冲池里面,如果是,就直接读取,不用再 次访问磁盘。
修改数据的时候,先修改缓冲池里面的页。内存的数据页和磁盘数据不一致的时候, 把它叫做脏页。InnoDB 里面有专门的后台线程把 Buffer Pool 的数据写入到磁盘, 每隔一段时间就一次性地把多个修改写入磁盘,这个动作就叫做刷脏
Buffer Pool 是 InnoDB 里面非常重要的一个结构,它的内部又分成几块区域。这里趁机到官网来认识一下 InnoDB 的内存结构和磁盘结构。

3.2. InnoDB 内存结构和磁盘结构

 
InnoDB的内存结构&磁盘结构
  • 3.2.1.内存结构:
    Buffer Pool 主要分为 3 个部分: Buffer PoolChange BufferAdaptive Hash Index,另外还有一个(redo)log buffer
    • 1、Buffer Pool:
      Buffer Pool 缓存的是页面信息,包括数据页、索引页。 查看服务器状态,里面有很多跟 Buffer Pool 相关的信息:
SHOW STATUS LIKE '%innodb_buffer_pool%';

这些状态都可以在官网查到详细的含义,用搜索功能。
Buffer Pool 默认大小是 128M(134217728 字节),可以调整。 查看参数(系统变量):

SHOW VARIABLES like '%innodb_buffer_pool%';

【思考🤔问题】:内存的缓冲池写满了怎么办?(Redis 设置的内存满了怎么办?)

InnoDB 用 LRU 算法来管理缓冲池(链表实现,不是传统的 LRU,分成了 young 和 old),经过淘汰的数据,剩下的就是热点数据。
内存缓冲区对于提升读写性能有很大的作用。

【思考🤔问题】:思考一个问题: 当需要更新一个数据页时,如果数据页在 Buffer Pool 中存在,那么就直接更新好了。 否则的话就需要从磁盘加载到内存,再对内存的数据页进行操作。也就是说,如果没有命中缓冲池,至少要产生一次磁盘 IO,有没有优化的方式呢?

  • 2、Change Buffer 写缓冲
    如果这个数据页不是唯一索引,不存在数据重复的情况,也就不需要从磁盘加载索引页判断数据是不是重复(唯一性检查)。这种情况下可以先把修改记录在内存的缓冲池中,从而提升更新语句(Insert、Delete、Update)的执行速度
    这一块区域就是 Change Buffer。5.5 之前叫 Insert Buffer 插入缓冲,现在也能支 持 delete 和 update。
    最后把 Change Buffer 记录到数据页的操作叫做 merge。什么时候发生 merge? 有几种情况:在访问这个数据页的时候,或者通过后台线程、或者数据库 shut down、 redo log 写满时触发
    如果数据库大部分索引都是非唯一索引,并且业务是写多读少,不会在写数据后立刻读取,就可以使用 Change Buffer(写缓冲)。写多读少的业务,调大这个值:
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';

代表 Change Buffer 占 Buffer Pool 的比例,默认 25%。

  • 3、Adaptive Hash Index
    索引应该是放在磁盘的,为什么要专门把一种哈希的索引放到内存?后续再补。
  • 4、(redo)Log Buffer

【思考🤔一个问题】:如果 Buffer Pool 里面的脏页还没有刷入磁盘时,数据库宕机或者重启,这些数据丢失。如果写操作写到一半,甚至可能会破坏数据文件导致数据库不可用?

【答案】:为了避免这个问题,InnoDB 把所有对页面的修改操作专门写入一个日志文件,并且在数据库启动时从这个文件进行恢复操作(实现 crash-safe)——用它来实现事务的持久性

 
crash-safe(崩溃恢复)

这个文件就是磁盘的 Redo Log(重做日志),对应于/var/lib/mysql/目录下的 ib_logfile0 和 ib_logfile1,每个 48M。
这种日志和磁盘配合的整个过程,其实就是 MySQL 里 WAL 技术 (Write-Ahead Logging),它的关键点就是先写日志,再写磁盘

show variables like 'innodb_log%';
含义
innodb_log_file_size 指定每个文件的大小,默认 48M
innodb_log_files_in_group 指定文件的数量,默认为 2
innodb_log_group_home_dir 指定文件所在路径,相对或绝对。如果不指定,则为 datadir 路径。

【思考🤔问题】:同样是写磁盘,为什么不直接写到 db file 里面去?为什么先写日志再写磁盘?

我们先来了解一下随机 I/O 和顺序 I/O的概念。 磁盘的最小组成单元是扇区,通常是 512 个字节。 操作系统和内存打交道,最小的单位是页 Page。 操作系统和磁盘打交道,读写磁盘,最小的单位是块 Block。

 
机器读取数据过程

如果所需要的数据是随机分散在不同页的不同扇区中,那么找到相应的数据需要等到磁臂旋转到指定的页,然后盘片寻找到对应的扇区,才能找到我们所需要的一块数据,一次进行此过程直到找完所有数据,这个就是随机 IO,读取数据速度较慢
假设已经找到了第一块数据,并且其他所需的数据就在这一块数据后边,那么就不需要重新寻址,可以依次拿到所需的数据,这个就叫顺序 IO
【答案】:刷盘是随机 I/O,而记录日志是顺序 I/O,顺序 I/O 效率更高。因此先把修改写入日志,可以延迟刷盘时机,进而提升系统吞吐
【优化】当然 Redo Log 也不是每一次都直接写入磁盘,在 Buffer Pool 里面有一块内存区域 (Log Buffer)专门用来保存即将要写入日志文件的数据,默认 16M,它一样可以节省磁盘 IO。

 

 
buffer pool 对Redo Log优化
SHOW VARIABLES LIKE 'innodb_log_buffer_size';

需要注意:Redo Log 的内容主要是用于崩溃恢复。磁盘的数据文件,数据来自 buffer pool。Redo Log 写入磁盘,不是写入数据文件。
那么,Log Buffer 什么时候写入 log file?
在写入数据到磁盘的时候,操作系统本身是有缓存的。flush 就是把操作系统缓冲区写入到磁盘。
log buffer 写入磁盘的时机,由一个参数控制,默认是 1。

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
含义
0(延迟写) log buffer 将每秒一次地写入 log file 中,并且 log file 的 flush 操作同时进行。 该模式下,在事务提交的时候,不会主动触发写入磁盘的操作。
1(默认,实时写,实时刷) 每次事务提交时 MySQL 都会把 log buffer 的数据写入 log file,并且刷到磁盘 中去。
2(实时写,延迟刷) 每次事务提交时 MySQL 都会把 log buffer 的数据写入 log file。但是 flush 操 作并不会同时进行。该模式下,MySQL 会每秒执行一次 flush 操作。
 
事务写logBuffer & osCache & 磁盘的控制

这是内存结构的第 4 块内容,redo log,它又分成内存和磁盘两部分。redo log 有什么特点?

  • 1、redo log 是 InnoDB 存储引擎实现的,并不是所有存储引擎都有。
  • 2、不是记录数据页更新之后的状态,而是记录这个页做了什么动作,属于物理日志。
  • 3、redo log 的大小是固定的,前面的内容会被覆盖。
     
    redo log 覆盖

    check point 是当前要覆盖的位置。如果 write pos 跟 check point 重叠,说明 redo log 已经写满,这时候需要同步 redo log 到磁盘中。

MySQL 的内存结构,总结一下,分为:Buffer pool、change buffer、Adaptive Hash Index、 log buffer。

  • 3.2.2、磁盘结构
    表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。InnoDB 的表空间分为 5 大类:
    • a、系统表空间 system tablespace
      在默认情况下 InnoDB 存储引擎有一个共享表空间(对应文件/var/lib/mysql/ ibdata1),也叫系统表空间。
      InnoDB 系统表空间包含InnoDB 数据字典双写缓冲区Change Buffer 和 Undo Logs,如果没有指定 file-per-table,也包含用户创建的表和索引数据。
      • 1、undo 在后面介绍,因为有独立的表空间。
      • 2、数据字典:由内部系统表组成,存储表和索引的元数据(定义信息)。
      • 3、双写缓冲(InnoDB 的一大特性):InnoDB 的页和操作系统的页大小不一致,InnoDB 页大小一般为 16K,操作系统页大小为 4K,InnoDB 的页写入到磁盘时,一个页需要分 4 次写。
         
        image.png

        如果存储引擎正在写入页的数据到磁盘时发生了宕机,可能出现页只写了一部分的情况,比如只写了 4K,就宕机了,这种情况叫做部分写失效(partial page write),可能会导致数据丢失
        【思考🤔】:不是有 redo log 吗?
        但是有个问题,如果这个页本身已经损坏了,用它来做崩溃恢复是没有意义的。所以在对于应用 redo log 之前,需要一个页的副本。如果出现了写入失效,就用页的副本来还原这个页,然后再应用 redo log这个页的副本就是 double write,InnoDB 的双写技术。通过它实现了数据页的可靠性。
        跟 redo log 一样,double write 由两部分组成,一部分是内存的 double write,
        一个部分是磁盘上的 double write。因为 double write 是顺序写入的,不会带来很大的 开销。
        在默认情况下,所有的表共享一个系统表空间,这个文件会越来越大,而且它的空间不会收缩。
      • b、独占表空间 file-per-table tablespaces
        让每张表独占一个表空间。这个开关通过 innodb_file_per_table 设置,默 认开启。
        开启后,则每张表会开辟一个表空间,这个文件就是数据目录下的 ibd 文件(例如 /var/lib/mysql/mysql/student_innodb.ibd),存放表的索引和数据。
        但是其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次 写缓冲(Double write buffer)等还是存放在原来的共享表空间内。
      • c、通用表空间 general tablespaces
        通用表空间也是一种共享的表空间,跟 ibdata1 类似。
        可以创建一个通用的表空间,用来存储不同数据库的表,数据路径和文件可以自定义。
        不同表空间的数据是可以移动的,删除表空间需要先删除里面的所有表。
      • d、临时表空间 temporary tablespaces
        存储临时表的数据,包括用户创建的临时表,和磁盘的内部临时表。对应数据目录 下的 ibtmp1 文件。当数据服务器正常关闭时,该表空间被删除,下次重新产生。
      • e、undo log tablespace
        undo log(撤销日志 or 回滚日志)记录了事务发生之前的数据状态(不包括select)。 如果修改数据时出现异常,可以用 undo log 来实现回滚操作(保持原子性)
        在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,属于逻辑格式的日志。
        redo Log 和 undo Log 与事务密切相关,统称为事务日志。
        undo Log 的数据默认在系统表空间 ibdata1 文件中,因为共享表空间不会自动收 缩,也可以单独创建一个 undo 表空间。

【思考🤔】有了这些日志之后,总结一下一个更新操作的流程?

update student set name = 'biudefu' where id=1;

1、事务开始,从内存或磁盘取到这条数据,返回给 Server 的执行器;
2、执行器修改这一行数据的值为 biudefu;
3、记录 name=biudefu 到 undo log;
4、记录 name=biudefu 到 redo log;
5、调用存储引擎接口,在内存(Buffer Pool)中修改 name=biudefu;
6、 事务提交。

  • 3.2.3、后台线程
    后台线程的主要负责刷新内存池(buffer pool)中的数据和把修改的数据页刷新到磁盘。后台线程分为:
    • 1、master thread(负责刷新缓存数据到磁盘并协调调度其它后台进程)
    • 2、IO thread(分为 insert buffer、log、read、write 进程。分别用来处理 insert buffer、 重做日志、读写请求的 IO 回调。)
    • 3、purge thread(用来回收 undo 页)
    • 4、page cleaner thread(用来刷新脏页)

除了 InnoDB 架构中的日志文件,MySQL 的 Server 层也有一个日志文件,叫做
binlog,它可以被所有的存储引擎使用。

3.3. Binlog

binlog 以事件的形式记录了所有的 DDL 和 DML 语句(因为它记录的是操作而不是数据值,属于逻辑日志),可以用来做主从复制和数据恢复。
跟 redo log 不一样,它的文件内容是可以追加的,没有固定大小限制
在开启了 binlog 功能的情况下,可以把 binlog 导出成 SQL 语句,把所有的过程操作重放一遍,来实现数据的恢复。
binlog 的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器的 binlog,然后执行一遍。
有了这两个日志之后,我们来看一下一条更新语句是怎么执行的:

 
sql执行整体流程

例如一条语句:update student set name='biudefu' where id=1;

1、先查询到这条数据,如果有缓存,也会用到缓存。
2、把 name 改成biudefu,然后调用引擎的 API 接口,写入这一行数据到内存,同时记录 redo log。这时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,可以随时提交。
3、执行器收到通知后记录 binlog,然后调用存储引擎接口,设置 redo log 为 commit 状态。
4、更新完成。

这张图片的重点:

  • 1、先记录到内存,再写日志文件。
  • 2、记录 redo log 分为两个阶段。
  • 3、存储引擎和 Server 记录不同的日志。
  • 4、先记录 redo,再记录 binlog。


参考资料:

《数据库查询优化器的艺术-原理解析与SQL性能优化》
《MySQL高性能书籍第3版(中文)》
《MySQL技术内幕-InnoDB存储引擎
第2版》



作者:biudefu
链接:https://www.jianshu.com/p/d4e6105ab4e5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

 

posted on 2020-11-25 23:30  秦羽的思考  阅读(303)  评论(0编辑  收藏  举报