一个查询语句经过哪些步骤

这次我们从 MySQL 的整体架构来讲 SQL 的执行过程,如下图:
MYSQL 查询和插入数据的流程是怎样的 - 图1
在整体分为两部分 Server 和引擎层,这里引擎层我使用 InnoDB 去代替,引擎层的设计是插件形式的,可以任意替代,接下来我们开始介绍每个组件的作用:

Server 层

连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接;
查询缓存:服务的查询缓存,如果能找到对应的查询,则不必进行查询解析,优化,执行等过程,直接返回缓存中的结果集;
解析器:解析器会根据查询语句,构造出一个解析树,主要用于根据语法规则来验证语句是否正确,比如 SQL 的关键字是否正确,关键字的顺序是否正确;
优化器:解析树转化为查询计划,一般情况下,一条查询可以有很多种执行方式,最终返回相同的结果,优化器就是根据成本找到这其中最优的执行计划;
执行器:执行计划调用查询执行引擎,而查询引擎通过一系列 API 接口查询到数据;

InnoDB

后台线程:负责刷新内存池中的数据,保证缓存池中的内存缓存是最近的数据,将已修改的数据刷新到磁盘文件,同时保证数据库发生异常的情况能恢复到正常情况;
内存池:内存池也可以叫做缓存池,主要为弥补磁盘的速度较慢对数据库产生的影响,查询的时候,首先将磁盘读到的页的数据放在内存池中,下次读取的时候直接从内存池中读取数据,修改数据的时候,首先修改内存池中的数据,然后后台线程按照一定的频率刷新到磁盘上。
文件:主要是指表空间文件,而外还有一些日志文件;
以上大致的介绍一下 MySQL 的整体架构,其中内存池、文件、后台线程等一些跟细节的东西没有介绍,后面我们介绍其他时候在带出来其中的详细的部分,另外在附上一张 MySQL5.6 整体架构图:
MYSQL 查询和插入数据的流程是怎样的 - 图2

InnoDB 如何保存数据

这部分内容是建立在上部分的基础上,需要对内存池、文件、后台线程深入到细节去了解组成,接下我们还是分三部分开始讲解:

文件

文件分为日志文件和存储文件,分为两部分讲起:

存储文件

存储文件也就是表数据的存储,整体的存储结构如下图:
MYSQL 查询和插入数据的流程是怎样的 - 图3
表空间主要分为两类文件,一类是共享表空间,一类是每张表单独的表空间,单独的表空间存放的是表中的数据、索引等信息,共享的表空间主要是存储事务信息、回滚信息等数据;表空间由段 (Segment)、区 (Extend)、页 (Page)、行 (Row) 组成,接下来简单介绍一下这 4 种结构:

  1. 段 (Segment)
    常见的 Segment 有数据段、索引段、回滚段等, 数据段为 B + 树的叶子节点 (Leaf node segment)、索引段为 B + 树的非叶子节点 (Non-leaf node segment)。如下图:
    MYSQL 查询和插入数据的流程是怎样的 - 图4
    每创建索引就会创建一个索引段,索引段的叶子节点指向数据段,通过这样的组合来完成我们查询数据时候需要,因此创建索引越多,会导致需要构建的索引段就越多,导致插入数据时间就会增加。
  2. 区 (Extend)
    区是构成段的基本元素,一个段由若干个区构成,一个区是物理上连续分配的一段空间,每一个段至少会有一个区,在创建一个段时会创建一个默认的区。如果存储数据时,一个区已经不足以放下更多的数据,此时需要从这个段中分配一个新的区来存放新的数据。一个段所管理的空间大小是无限的,可以一直扩展下去,但是扩展的最小单位就是区。每个区大小固定为 1MB,区由页组成,为保证区中 Page 的连续性通常 InnoDB 会一次从磁盘中申请 4-5 个区。在默认 Page 的大小为 16KB 的情况下,一个区则由 64 个连续的 Page 组成。
  3. 页 (Page):
    页是构成区的基本单位,是 InnoDB 磁盘管理的最小单位。在逻辑上(页面号都是从小到大连续的)及物理上都是连续的。在向表中插入数据时,如果一个页面已经被写完,系统会从当前区中分配一个新的空闲页面处理使用,如果当前区中的 64 个页面都被分配完,系统会从当前页面所在段中分配一个新的区,然后再从这个区中分配一个新的页面来使用。
  4. 行 (Row):
    InnoDB 按照行进行存放数据,每个页存放的数据有硬性规定,最多存放 16KB,当数据大于 16KB 的时候会发生行溢出,会存储到而外的页 (Uncompressed BLOB Page) 当中。

    日志文件

    关于日志文件这里主要介绍三种日志文件,分别为 binlog、redo log、redo log:

    binlog

    binlog 用于记录数据库执行的写入性操作 (不包括查询) 信息,以二进制的形式保存在磁盘中。binlog 是 mysql 的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志。binlog 是通过追加的方式进行写入的,可以通过 max_binlog_size 参数设置每个 binlog 文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

    binlog 日志格式
  5. ROW
    基于行的复制,不记录每条 sql 语句的上下文信息,仅需记录哪条数据被修改了。
    优点:
    不会出现某些特定情况下的存储过程、或 function、或 trigger 的调用和触发无法被正确复制的问题;
    缺点:
    因为每行都要记录日志,会照成日志量暴涨;

  6. STATMENT
    基于 SQL 语句的复制,每一条会修改数据的 sql 语句会记录到 binlog 中。
    优点:
    不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO, 从而提高了性能;
    缺点:
    在某些情况下会导致主从数据不一致,比如执行 sysdate () 等函数的时候。
  7. MIXED
    基于 STATMENT 和 ROW 两种模式的混合复制,一般的复制使用 STATEMENT 模式保存 binlog,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog

    使用场景

    binlog 的主要使用场景有两个,分别是主从复制和数据恢复;

  8. 主从复制
    在 Master 端开启 binlog,然后将 binlog 发送到各个 Slave 端,Slave 端重放 binlog 从而达到主从数据一致。

  9. 数据恢复
    恢复到某一时刻的日志,通过使用 mysqlbinlog 工具来恢复数据;
    刷盘时机
    对于 InnoDB 存储引擎而言,只有在事务提交时才会记录 biglog,此时记录还在内存中,Mysql 通过 sync_binlog 参数控制 biglog 的刷盘时机,取值范围是 0-N,
    N 代表多少条以后开始进行刷盘,当设置为 0 的时候由系统自行判断何时写入磁盘,当设置为 1 的时候,相当于每次 Commit 就进行刷盘一次,但是这个时候要注意与 redo log 日志可能存在不一致的情况,这个时候需要设置 innodb_support_xa 参数也为 1,这样就能保证两个两份日志是同步的。

binlog

binlog 用于记录数据库执行的写入性操作 (不包括查询) 信息,以二进制的形式保存在磁盘中。 binlog 是 mysql
的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志。

  • 逻辑日志: 可以简单理解为记录的就是 sql 语句 。
  • 物理日志: mysql 数据最终是保存在数据页中的,物理日志记录的就是数据页变更 。

binlog 是通过追加的方式进行写入的,可以通过 max_binlog_size 参数设置每个 binlog
文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

binlog 使用场景

在实际应用中, binlog 的主要使用场景有两个,分别是 主从复制数据恢复

  1. 主从复制 :在 Master 端开启 binlog ,然后将 binlog 发送到各个 Slave 端, Slave 端重放 binlog 从而达到主从数据一致。
  2. 数据恢复 :通过使用 mysqlbinlog 工具来恢复数据。
    binlog 刷盘时机
    对于 InnoDB 存储引擎而言,只有在事务提交时才会记录 biglog ,此时记录还在内存中,那么 biglog
    是什么时候刷到磁盘中的呢? mysql 通过 sync_binlog 参数控制 biglog 的刷盘时机,取值范围是 0-N
  • 0:不去强制要求,由系统自行判断何时写入磁盘;
  • 1:每次 commit 的时候都要将 binlog 写入磁盘;
  • N:每 N 个事务,才会将 binlog 写入磁盘。

从上面可以看出, sync_binlog 最安全的是设置是 1 ,这也是 MySQL 5.7.7
之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。

binlog 日志格式

binlog 日志有三种格式,分别为 STATMENT 、 ROW 和 MIXED 。
在 MySQL 5.7.7 之前,默认的格式是 STATEMENT , MySQL 5.7.7 之后,默认值是 ROW 。日志格式通过 binlog-format 指定。

  • STATMENT : 基于 SQL 语句的复制 ( statement-based replication, SBR ),每一条会修改数据的 sql 语句会记录到 binlog 中 。 优点: 不需要记录每一行的变化,减少了binlog 日志量,节约了 IO , 从而提高了性能; 缺点: 在某些情况下会导致主从数据不一致,比如执行sysdate()slepp() 等 。
  • ROW : 基于行的复制 ( row-based replication, RBR ),不记录每条 sql 语句的上下文信息,仅需记录哪条数据被修改了 。
    • 优点: 不会出现某些特定情况下的存储过程、或 function、或 trigger 的调用和触发无法被正确复制的问题 ;
    • 缺点: 会产生大量的日志,尤其是 alter table 的时候会让日志暴涨
  • MIXED : 基于 STATMENT 和 ROW 两种模式的混合复制 ( mixed-based replication, MBR ),一般的复制使用 STATEMENT 模式保存 binlog ,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog

redo log

为什么需要 redo log

我们都知道,事务的四大特性里面有一个是 持久性 ,具体来说就是
只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态 。那么 mysql
是如何保证一致性的呢?最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面:

  1. 因为 Innodb 是以 页 为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!
  2. 一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机 IO 写入性能太差!

因此 mysql 设计了 redo log , 具体来说就是只记录事务对数据页做了哪些修改
,这样就能完美地解决性能问题了 (相对而言文件更小并且是顺序 IO)。

redo log 基本概念

redo log 包括两部分:一个是内存中的日志缓冲 ( redo log buffer ),另一个是磁盘上的日志文件 ( redo log<br />file )。 mysql 每执行一条 DML 语句,先将记录写入 redo log buffer
,后续某个时间点再一次性将多个操作记录写到 redo log file 。这种 先写日志,再写磁盘 的技术就是 MySQL
里经常说到的 WAL(Write-Ahead Logging) 技术。
在计算机操作系统中,用户空间 ( user space ) 下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间 ( <br />kernel space )缓冲区( OS Buffer )。因此, redo log buffer 写入 redo log<br />file 实际上是先写入 OS Buffer ,然后再通过系统调用 fsync() 将其刷到 redo log file
中,过程如下:
image.png
mysql 支持三种将 redo log buffer 写入 redo log file 的时机,可以通过 <br />innodb_flush_log_at_trx_commit 参数配置,各参数值含义如下:

参数值 含义
0(延迟写) 事务提交时不会将 redo log buffer 中日志写入到 os buffer ,而是每秒写入 os buffer 并调用 fsync() 写入到 redo log file 中。也就是说设置为 0 时是 (大约) 每秒刷新写入到磁盘中的,当系统崩溃,会丢失 1 秒钟的数据。
1(实时写,实时刷) 事务每次提交都会将 redo log buffer 中的日志写入 os buffer 并调用 fsync() 刷到 redo log file 中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO 的性能较差。
2(实时写,延迟刷) 每次提交都仅写入到 os buffer ,然后是每秒调用 fsync() 将 os buffer 中的日志写入到 redo log file 。

image.png

redo log 记录形式

前面说过, redo log 实际上记录数据页的变更,而这种变更记录是没必要全部保存,因此 redo log
实现上采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志。如下图:
image.png
同时我们很容易得知, 在 innodb 中,既有 redo log 需要刷盘,还有 数据页 也需要刷盘, redo log 存在的意义主要就是降低对 数据页 刷盘的要求 。在上图中, write pos 表示 redo log 当前记录的 LSN (逻辑序列号) 位置, check point 表示 数据页更改记录 ** 刷盘后对应 redo log 所处的 LSN (逻辑序列号) 位置。 write pos 到 check point 之间的部分是 redo log 空着的部分,用于记录新的记录; check point 到 write pos 之间是 redo log 待落盘的数据页更改记录。当 write pos 追上 check point 时,会先推动 check point 向前移动,空出位置再记录新的日志。
启动 innodb 的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。因为 redo log 记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志 (如 binlog ) 要快很多。 重启 innodb 时,首先会检查磁盘中数据页的 LSN ,如果数据页的 LSN 小于日志中的 LSN ,则会从 checkpoint 开始恢复。 还有一种情况,在宕机前正处于
checkpoint 的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度,此时会出现数据页中记录的 LSN 大于日志中的 LSN
,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。

redo log 与 binlog 区别
redo log binlog
文件大小 redo log 的大小是固定的。 binlog 可通过配置参数 max_binlog_size 设置每个 binlog 文件的大小。
实现方式 redo log 是 InnoDB 引擎层实现的,并不是所有引擎都有。 binlog 是 Server 层实现的,所有引擎都可以使用 binlog 日志
记录方式 redo log 采用循环写的方式记录,当写到结尾时,会回到开头循环写日志。 binlog 通过追加的方式记录,当文件大小大于给定值后,后续的日志会记录到新的文件上
适用场景 redo log 适用于崩溃恢复 (crash-safe) binlog 适用于主从复制和数据恢复

由 binlog 和 redo log 的区别可知: binlog 日志只用于归档,只依靠 binlog 是没有 <br />crash-safe 能力的。但只有 redo log 也不行,因为 redo log 是 InnoDB
特有的,且日志上的记录落盘后会被覆盖掉。因此需要 binlog 和 redo log
二者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。

redo log

redo log 包括两部分:redo log buffer 和 redo log file,redo log buffer 是在内存中,redo log file 是在磁盘上,当 MySQL 执行 DML 语句的时候,首先写入 redo log buffer,然后按照一定条件顺序写入 redo log file,什么时候会触发 buffer 内容写入到 file 当中呢?

  1. InnoDB 后台线程中的主线程,每秒会进行一次将 buffer 中的数据刷入到磁盘当中;
  2. 通过设置 innodb_flush_log_at_trx_commit 参数,来控制刷新的时机,当设置为 1 的时候,事务每次提交都会将 log buffer 中的日志写入 os buffer 并调用 fsync () 刷到 log file on disk 中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO 的性能较差。当设置为 0 的时候,事务提交时不会将 log buffer 中日志写入到 os buffer,而是每秒写入 os buffer 并调用 fsync () 写入到 log file on disk 中。也就是说设置为 0 时是 (大约) 每秒刷新写入到磁盘中的,当系统崩溃,会丢失 1 秒钟的数据。当设置为 2 的时候,每次提交都仅写入到 os buffer,然后是每秒调用 fsync () 将 os buffer 中的日志写入到 log file on disk。
    MYSQL 查询和插入数据的流程是怎样的 - 图8

    redo log 日志格式

    redo log 记录数据页的变更,在设计上 redo log 采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志,本质上就是一个环状。
    MYSQL 查询和插入数据的流程是怎样的 - 图9
    rdo log 刷盘完成以后,其实数据最终还没刷新到真正数据磁盘上,因此还需要刷新到真正的数据磁盘上,本质上 redo log 的设计就是为了降低对数据页刷盘的要求,接下来我们结合上图来聊聊是如何刷新到数据文件文件上的,也就是 checkpoint 机制:
    首先看下环,环上有 4 个 iblogfile* 的文件,该文件就是存储 redo log 日志的文件,可以通过控制 innodb_log_files_in_group 的数量来控制文件的个数,通过 innodb_log_file_size 来控制文件的大小,不介意将文件的设置的太大,如果设置的太大会导致奔溃恢复的时候过于缓慢,也不能设置的太小,这样可能导致一次事务需要切换多次日志文件,此外还会照成频繁写入磁盘文件,照成性能抖动;
    接下来我们看两个端点 write pos 和 check point,write pos 到 check point 之间的部分是 redo log 空着的部分,用于记录新的记录;check point 到 write pos 之间是 redo log 待落盘的数据页更改记录。当 write pos 追上 check point 时,会先推动 check point 向前移动,空出位置再记录新的日志。
    InnoDB 在启动的时候,不管上次数据库是否正常关闭,都会尝试进行恢复操作,分为两种情况:

  3. checkpoint 表示已经完整刷到磁盘上 data page 上的 LSN,因此恢复时仅需要恢复从 checkpoint 开始的日志部分,LSN 表示写入日志的字节的总量,例如,当数据库在上一次 checkpoint 的 LSN 为 10000 时宕机,且事务是已经提交过的状态。启动数据库时会检查磁盘中数据页的 LSN,如果数据页的 LSN 小于日志中的 LSN,则会从检查点开始恢复。

  4. 在宕机前正处于 checkpoint 的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度。这时候一宕机,数据页中记录的 LSN 就会大于日志页中的 LSN,在重启的恢复过程中会检查到这一情况,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。

在恢复的过程中因为 redo log 记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志 (如 binlog) 要快很多;

使用的场景

MySQL 用来确保事务的持久性。redo log 记录事务执行后的状态,用来恢复未写入 data file 的已成功事务更新的数据。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 mysql 服务的时候,根据 redo log 进行重做,从而达到事务的持久性这一特性。

undo log

undo log 记录数据的逻辑变化,用户事务的回滚操作和 MVCC, undo log 存放在共享表空间中,以段 (rollback segment) 的形式存在。

undo log 日志格式

逻辑格式的日志,在事务进行回滚的时候,可以将数据从逻辑上恢复至事务之前的状态。

使用的场景

保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。

刷盘时机

当事务提交之后,undo log 并不能立马被删除,而是放入待清理的链表,由 Purge 线程判断是否由其他事务在使用 undo 段中表的上一个事务之前的版本信息,决定是否可以清理 undo log 的日志空间。

内存池

InnoDB 存储引擎是基于磁盘存储的,也就是说数据都是存储在磁盘上的,由于 CPU 速度和磁盘速度之间的鸿沟,InnoDB 引擎使用缓冲池技术来提高数据库的整体性能。内存池简单来说就是一块内存区域。在数据库中进行读取页的操作,首先将从磁盘读到的页存放在内存池中,下一次读取相同的页时,首先判断该页是不是在内存池中,若在,称该页在内存池中被命中,直接读取该页。否则,读取磁盘上的页。对于数据库中页的修改操作,首先修改在内存池中页,然后再以一定的频率刷新到磁盘,并不是每次页发生改变就刷新回磁盘。
MYSQL 查询和插入数据的流程是怎样的 - 图10
内存池中缓存的信息主要有:index page、data page、insert buffer、自适应哈希索引、 lock info、数据字典信息等。索引页和数据页占缓冲池的很大一部分。在 InnoDB 中,内存池中的页大小默认为 16KB,和磁盘的页的大小默认一样。我们已经介绍过数据文件的存储结构相信大家对缓存结构的内容也会有一定理解,我们就不单独介绍了,后面只会重点强调一下 insert buffer 和自适应哈希索引这两块内容,以及扩展下内存池的设计原理。

Insert Buffer

Insert Buffer 的设计,对于非聚集索引的插入和更新操作,不是每一次直接插入到索引页中,而是先判断插入非聚集索引页是否在缓冲池中,若存在,则直接插入,不存在,则先放入一个 Insert Buffer 对象中。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行 Insert Buffer 和辅助索引页子节点的 merge(合并)操作,这时通常能将多,这就大大提高了对于非聚集索引插入的性能。这个时候可能会照成一种情况,当 MySQL 数据库发生宕机的时候有有大量的 Insert Buffer 没有被合并到非聚集索引的页当中的时候,这个时候 MySQL 恢复需要很长的时间。
需要满足的条件:
索引是非聚集索引,索引不是唯一的;
对于具体的实现我们下次再聊;

自适应哈希索引

InnoDB 存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以提升速度,这简历哈希索引,称之为自适应哈希索引。AHI 是通过缓冲池的 B + 树页构造而来的。因此建立的速度非常快,且不要对整张表构建哈希索引。InnoDB 存储引擎会自动根据访问的频率和模式来自动的为某些热点页建立哈希索引。

后台线程

Master Thread

这是最核心的一个线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括赃页的刷新、合并插入缓冲等。

IO Thread

在 InnoDB 存储引擎中大量使用了异步 IO 来处理写 IO 请求,IO Thread 的工作主要是负责这些 IO 请求的回调处理。

Purge Thread

事务被提交之后,undo log 可能不再需要,因此需要 Purge Thread 来回收已经使用并分配的 undo 页. InnoDB 支持多个 Purge Thread, 这样做可以加快 undo 页的回收。
完成整体功能介绍以后,我们开始聊聊数据如何插入到 InnoDB 引擎上的:
假设场景如下:
首先我们创建一张表 T,主键为 Id,辅助索引为 a
create table T(id int primary key, a int not null, name varchar(16),index (a))engine=InnoDB;
接下来插入一条数据,
insert into t (id,a,name) values (id1,a1,’ 哈哈 ‘),(id2,a2,’ 哈哈哈 ‘);
我们介绍过 MySQL 读取数据的流程,Server 层我们还是会经过连接器、解析器、优化器、执行器这些东西,这些我们就不介绍了,我们主要介绍剩下的操作:
插入数据时候可能有两种场景:
第一种场景:假设 Id1 这条数据在内存池中,

  1. 直接更新 Buffer Pool 中的 Index Page 和 Data Page;
  2. 写入 redo log 中,处于预提交状态;
  3. 写入 binlog 中,
  4. 提交事务,处于 commit 状态,两阶段提交;
  5. 后台线程写入到数据文件的索引段和数据段中;

第二种场景假设 id2 这条数据不再内存池中,

  1. 数据写入到内存池中,非聚集索引写入到 Insert Buffer,其他数据写入 Data Page 中;
  2. 后续的动作保持和上面剩下的步骤一样。

    扩展阅读

    我们来聊聊内存池 (Buffer Pool) 运行原理,可以从以下 3 个方面来看:

  3. 如何管理缓存的页?
    InnoDB 为每一个缓存页都创建了一些控制信息,这些控制信息包括该页所属的表空间编号、页号、页在 Buffer Pool 中的地址、LSN 等信息,每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,所以整个 Buffer Pool 对应的内存空间看起来就是这样的:
    MYSQL 查询和插入数据的流程是怎样的 - 图11
    碎片就是空间不够分配的缓存页。
    当我们最初启动 MySQL 服务器的时候,需要完成对 Buffer Pool 的初始化过程,就是分配 Buffer Pool 的内存空间,把它划分成若干对控制块和缓存页。但是此时并没有真实的磁盘页被缓存到 Buffer Pool 中,之后随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中,接下来会有一个问题就是怎么区分 Buffer Pool 中哪些缓存页是空闲的,哪些已经被使用?我们最好在某个地方记录一下哪些页是可用的,我们可以把所有空闲的页包装成一个节点组成一个链表,这个链表也可以被称作 Free 链表。因为刚刚完成初始化的 Buffer Pool 中所有的缓存页都是空闲的,所以每一个缓存页都会被加入到 Free 链表中,整体设计如下图:
    MYSQL 查询和插入数据的流程是怎样的 - 图12
    从图中可以看出,Free 链表包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。每个 Free 链表的节点中都记录了某个缓存页控制块的地址,而每个缓存页控制块都记录着对应的缓存页地址,所以相当于每个 Free 链表节点都对应一个空闲的缓存页。
    每当需要从磁盘中加载一个页到 Buffer Pool 中时,就从 Free 链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的 Free 链表节点从链表中移除,表示该缓存页已经被使用了。

  4. 缓存的淘汰?
    机器的内存大小是有限的,所以 MySQL 的 InnoDB Buffer Pool 的大小同样是有限的,如果需要缓存的页占用的内存大小超过了 Buffer Pool 大小,InnoDB Buffer Pool 采用经典的 LRU 算法来进行页面淘汰,以提高缓存命中率。当 Buffer Pool 中不再有空闲的缓存页时,就需要淘汰掉部分最近很少使用的缓存页。
    当我们需要访问某个页时,可以这样处理 LRU 链表:
    1. 如果该页不在 Buffer Pool 中,在把该页从磁盘加载到 Buffer Pool 中的缓存页时,就把该缓存页包装成节点塞到链表的头部。
    2. 如果该页在 Buffer Pool 中,则直接把该页对应的 LRU 链表节点移动到链表的头部。
    但是这样做会有一些性能上的问题,比如你的一次全表扫描或一次逻辑备份就把热数据给冲完了,就会导致导致缓冲池污染问题!Buffer Pool 中的所有数据页都被换了一次血,其他查询语句在执行时又得执行一次从磁盘加载到 Buffer Pool 的操作,而这种全表扫描的语句执行的频率也不高,每次执行都要把 Buffer Pool 中的缓存页刷新一次,这严重的影响到其他查询对 Buffer Pool 的使用,降低了缓存命中率。
    针对这种场景 InnoDB 存储引擎对传统的 LRU 算法做了一些优化,在 InnoDB 中加入了 midpoint。新读到的页,虽然是最新访问的页,但并不是直接插入到 LRU 列表的首部,而是插入 LRU 列表的 midpoint 位置。这个算法称之为 midpoint insertion stategy。默认配置插入到列表长度的 5/8 处。midpoint 由参数 innodb_old_blocks_pct 控制。
    midpoint 之前的列表称之为 new 列表,之后的列表称之为 old 列表。可以简单的将 new 列表中的页理解为最为活跃的热点数据。
  5. 脏页如何实现刷新?
    更新是在缓存池中先进行的,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页。所以需要考虑这些被修改的页面什么时候刷新到磁盘?当然,最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。所以每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步,由后台刷新线程依次刷新到磁盘,实现修改落地到磁盘。
    但是如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道 Buffer Pool 中哪些页是脏页,哪些页从来没被修改过呢?我们需要创建一个存储脏页的链表,凡是在 LRU 链表中被修改过的页都需要加入这个链表中,因为这个链表中的页都是需要被刷新到磁盘上的,所以也叫 Flush 链表,链表的构造和 Free 链表差不多,这里的脏页修改指的此页被加载进 Buffer Pool 后第一次被修改,只有第一次被修改时才需要加入 Flush 链表,如果这个页被再次修改就不会再放到 Flush 链表了,因为已经存在。需要注意的是,脏页数据实际还在 LRU 链表中,而 Flush 链表中的脏页记录只是通过指针指向 LRU 链表中的脏页。

    结束

    参考文档

https://www.cnblogs.com/wtzbk/p/14410608.html