MySQL(四):InnoDB引擎底层解析

  官方文档地址:https://dev.mysql.com/doc/refman/8.3/en/innodb-storage-engine.html

  InnoDB存储引擎有三大特性:双写机制、Buffer Pool、自适应Hash。

  InnoDB存储引擎架构的内存和磁盘结构如下:

0

  上述架构图描述了 数据在内存和磁盘上的流转和存储流程,在实际开发过程中,用户只负责使用客户端发送请求并等待服务器返回结果。表中的数据存储在哪里?以什么格式存放?InnoDB以什么方式访问这些数据?这些都是InnoDB内部处理并且对用户透明的,下面逐步来学习InnoDB到底是如何实现这些功能的。

1、InnoDB记录存储结构和索引页结构

  InnoDB是一个会将数据存储到磁盘上的存储引擎,即使服务关机重启后数据依然存在。数据处理过程发生在内存中,所以需要把磁盘中的数据加载到内存中,若处理写入或修改请求,还需将内存中的内容刷新到磁盘上。

0

  磁盘和内存的读取速度相差几个数量级,当从表中获取某些记录时,InnoDB将 数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为16KB。在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

0

  数据是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式。

 

1.1、行格式

 

  InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact、Redundant、Dynamic和Compressed行格式。可通过如下命令查看默认值:

 

 

show variables like 'innodb_default_row_format'

 

   可以在创建或修改表的语句中指定行格式:

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称

1.1.1、COMPACT

  COMPACT行格式示意图如下:

0

·变长字段长度列表

  MySQL支持一些变长的数据类型,如VARCHAR(M)、各种TEXT类型、各种BLOB类型,将拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据时不固定的,  所以在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。若该可变字段允许存储的最大字节数(M×W)超过255字节并且真实存储的字节数(L)超过127字节,则使用2个  字节,否则使用1个字节。

  变长字段实际存储字节。

·NULL值列表

  表中的某些列可能存储NULL值,若把这些NULL值都放到记录的真实数据中存储会很占地方,Compact行格式把这些值为NULL的列统一管理起来,存储到NULL值列表。每个允许存储NULL的列对应一个二进制位,二进制位的值为1时,代表该列的值为NULL。二进制位的值为0时,代表该列的值不为NULL。

·记录头信息

  它是由固定的5个字节组成。5个字节,即40个二进制位,不同的位代表不同的含义。

 
二进制位数
解释
预留位1
1
没有使用
预留位2
1
没有使用
delete_mask
1
标记该记录是否被删除
min_rec_mask
1
B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned
4
表示当前记录拥有的记录数
heap_no
13
表示当前记录在页的位置信息
record_type
3
表示当前记录的类型, 0表示普通记录, 1表示B+树非叶子节点记录, 2表示最小记录, 3表示最大记录
next_record
16
表示下一条记录的相对位置

1.1.2、隐藏列信息

  MySQL会为每个记录默认的添加一些列(也称为隐藏列),包括:

字段名
是否必输
长度
含义
DB_ROW_ID(row_id)
非必须
6字节
表示行ID,唯一标识一条记录
DB_TRX_ID
必须
6字节
表示事务ID
DB_ROLL_PTR
必须
7字节
表示回滚

  InnoDB对主键的生成策略是:优先使用用户自定义主键作为主键,若用户没有定义主键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为row_id的隐藏列作为主键。

1.1.3、Dynamic和Compressed行格式

  MySQL5.7的默认行格式就是Dynamic,Dynamic和Compressed行格式和Compact行格式类似,只是在处理 行溢出 数据时不同。唯一不同的是:Compressed行格式会采用压缩算法对页面进行压缩,以节省空间。

 

 

1.1.4、数据溢出

 

  定义一个表,表中只有一个VARCHAR字段,如下:

CREATE TABLE test( c VARCHAR(65000) )

  然后往这个字段插入65000个字符,这时会发生什么?

  MySQL中磁盘和内存交互的基本单位是页,即MySQL是以页为基本单位来管理存储空间的,数据记录都会被分配到某个页中存储。而一个页的大小一般是16KB,即16384字节,而一个VARCHAR(M)类型的列最多可以存储65532个字节,如此就可能造成一个页存放不了一条记录的情况。

  在Compact和Redundant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的前768个字节的数据,然后把剩余的数据分散存储在几个其他的页中,记录的真实数据处用20个字节存储指向这些页的地址。这个过程也叫做行溢出,存储超出768字节的那些页面也被称为溢出页。

  Dynamic和Compressed行格式,不会在记录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。

1.2、索引页格式

  InnoDB管理存储空间的基本单位,一个页的大小一般是16KB。

  InnoDB为了不同的目的而设计了许多不同类型的页,存放记录的页为索引(INDEX)页。

1.2.1、数据页结构

  0

  一个InnoDB数据页的存储空间大致被划分成了7个部分:

 0

1.1、User Records

  存储的记录会按照指定的行格式存储到User Records部分。在一开始生成页的时候,并没有User Records这个部分。

  每当插入一条记录,都会从Free Space部分,即尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替换掉之后,表示该页已经使用完,若还有新的记录插入就需要去申请新的页。

  当记录被删除时,则会修改记录头信息中的 delete_mask 为1,被删除的记录还在页中,还在真实的磁盘上。被删除的记录不立即从磁盘上移除,是因为移除它们后把其他的记录在磁盘上重新排列需要性能消耗。

  打一个删除标记,所有被删除掉的记录都会组成一个垃圾链表,该链表中的记录占用的空间称为可重用空间,若有新记录插入到表中,可能把这些被删除的记录占用的存储空间覆盖掉。

  插入的记录在会记录自己在本页的位置,写入了记录头信息中heap_no部分。heap_no值为0和1的记录是InnoDB自动给每个页增加的两个记录,称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录,一个代表最大记录,这两条存放在页的User Records部分,被单独放在一个称为 Infimum + Supremum 的部分。

  记录头信息中next_record记录了从当前记录的真实数据到下一条记录的真实数据的地址偏移量。本质上是链表,可以通过一条记录找到它的下一条记录,但是下一条记录指得并不是按照我们插入顺序的下一条记录,是按照主键值由小到大的顺序的下一条记录。

  Infimum记录(最小记录)的下一条记录是本页中主键值最小的用户记录,本页中主键值最大的用户记录的下一条记录了就是Supremum记录(最大记录)。

  0

  记录是按照主键从小到大的顺序形成了一个单链表,记录被删除,则从这个链表上摘除。

1.2、Page Directory

  Page Directory主要是解决记录链表的查找问题,按链表查找的办法:从Infimum记录(最小记录)开始,沿着链表一直往后找,总会找到或找不到。

InnoDB为页中的记录再制作了一个目录,过程如下:

  ·将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组;

  ·每个组的最后一条记录(即组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,即该组内共有几条记录;

  ·每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,就是所谓的Page Directory,即页目录页面目录中的这些地址偏移量被称为槽(Slot),该页面目录是由槽组成的。

 0

  ·每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有1条记录,最大记录所在的分组拥有的记录条数只能在1到8条之间,剩下的分组中记录的条数范围只能在是4到8条之间,如下图:

0

  一个数据页中查找指定主键值的记录的过程分为两步:

  通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。

  通过记录的next_record属性遍历该槽所在的组中的各个记录。

1.3、Page Header

  InnoDB为了能得到一个数据页中存储的记录的状态信息,如本页中已存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等,在页中定义了一个叫Page Header的部分,这是页结构的第二部分,这部分占用固定的56个字节,存储各种状态信息。

1.4、File Header

  File Header针对各种类型的页都通用,即不同类型的页都会以File Header作为第一个组成部分,描述了一些针对各种页都通用的一些信息,如页的类型,页的编号是多少,它的上一页、下一页,页的校验和等,该部分占用固定的38个字节。

  页的类型,包括Undo日志页、段信息节点、InsertBuffer空闲列表,InsertBuffer位图、系统页、事务系统数据、表空间头部信息、拓展描述页、溢出页、索引页等。

上一页、下一页通过建立一个双向链表把许多的页串联起来,这些页无需在物理上真正连着,所有的数据页其实是一个双向链表。

1.5、File Trailer

  为了检测一个页是否完整(在同步的时候有没有发生只同步一半的情况),InnoDB每个页的尾部都加了一个File Trailer部分,这个部分由8个字节组成,可以分成2个小部分:

  前4个字节代表页的校验和,后4个字节代表页面被最后修改时对应的日志序列位置(LSN),这个和校验页的完整性有关。

  这部分是和File Header中的校验和相对应的,每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,若完全同步成功,则页的首部和尾部的校验和应该是一致的。若写一半断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trailer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。

 

 

2、InnoDB的表空间

 

  系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为 表名.idb 的实际文件。表空间被切分为许许多多页的池子,当想为某个表插入一条记录的时候,从池子中捞出一个对应的页将数据写进去。

 

  InnoDB是以页为单位管理存储空间的,聚簇索引(完整的表数据)和其他的二级索引都是以B+树的形式保存到表空间的,而B+树的节点就是数据页。

 

  任何类型的页都有File Header这部分,File Header中专门的地方(FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID)保存页属于哪个表空间,同时表空间中的每一个页都对应着一个页号(FIL_PAGE_OFFSET),这个页号由4个字节组成,即32个比特位,一个表空间最多可以拥有2³²个页,若按照页的默认大小16KB计算,一个表空间最多支持64TB的数据。

 

    0

 

2.1、独立表空间结构

 

2.1.1、区(extent)

 

  表空间的页可以达到2³²个页,为了更好的管理这些页面,InnoDB中提出了区(extent)的概念。对于16KB的页来说,连续的64个页就是一个区,即一个区默认占用1MB空间大小。

 

  不管是系统表空间还是独立表空间,可以看成是由若干个区组成的,每256个区又被划分成一个组。

 

  第一个组最开始的3个页面的类型是固定的:用来登记整个表空间的一些整体属性以及本组所有的区被称为FSP_HDR,即extend 0 ~ extend 255 这 256个区,整个表空间只有一个FSP_HDR。

 

  其余各组最开始的2个页面的类型是固定的,一个XDES类型,用来登记本组256个区的属性,FSP_HDR类型的页面其实和XDES类型的页面的作用类似,不过FSP_HDR类型的页面还会额外存储一些表空间的属性。

 

  引入区的主要目的:消除随机IO

 

  每向表中插入一条记录,本质上就是该表的聚簇索引以及所有二级索引代表的B+树的节点中插入数据,而B+树的每一层中的页都会形成一个双向链表,若是以页的单位来分配存储空间,双向链表相邻的两个页之间的物理位置可能离的非常远。

 

  B+树索引的适用场景时,范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描,若链表中相邻的两个页物理位置相距很远,就是所谓的随机I/O。

 

  一个区是在物理位置上连续的64个页。在表中数据量大的时候,为某个索引分配空间时就不再按照页为单位分配了,而是按照区为单位分配,可以一次性分配多个连续的区,从性能角度看,可以消除很多的随机I/O。

 

2.1.2、段(segment)

 

  InnoDB对B+树的叶子节点和非叶子节点进行了区别对待,即叶子节点、非叶子节点都有独有的区。

 

  存放叶子节点的区的集合为一个段(segment),存放非叶子节点的区的集合也是一个段。一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。

 

  段不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念。

 

2.2、系统表空间

 

2.2.1、整体结构

 

  整个MySQL进行只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,这些页面相当于表空间之首,所以它的表空间ID(Space ID)是0。

 

  系统表空间的 extend 1 和 extend 2 这两个区,就是页号从 64 ~ 191 这128个页面被称为 Doublewrite buffer,即双写缓冲区。

 

2.2.2、双写缓冲区/双写机制

 

  双写缓冲区/双写机制是InnoDB的三大特性之一,另外两个是Buffer Pool、自适应Hash索引。

 

  这是一种特殊文件flush技术,带给InnoDB存储引擎的是数据页的可靠性。它的作用是,在把页写到数据文件之前,InnoDB先将它们写到一个叫 doublewrite buffer(双写缓冲区)的连续区域内,在写doublewrite buffer完成后,InnoDB才会把页写到数据文件的适当的位置。若在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer中找到完好的page副本用于恢复。

 

  doublewrite buffer是InnoDB在系统表空间上的128个页(2个区,extend1和extend2),大小是2MB。

 

  MySQL写数据页时,会写两遍到磁盘上,第一遍是写到 doublewrite buffer,第二遍是写到真正的数据文件中。若发生了极端情况(断电),InnoDB再次启动后,发现了一个页数据已经损坏,此时就可以从 doublewrite buffer 中进行数据恢复了。

 

  双写缓冲区不仅在内存中有,更多的是属于MySQL的系统表空间,属于磁盘文件的一部分,那么为什么要引入一个双写机制?

 

  InnoDB的页大小一般是16KB,其数据校验也是针对这16KB来计算的,将数据写入到磁盘是以页为单位进行操作的,而操作系统写文件是以4KB作为单位的,每写一个InnoDB的页到磁盘上,操作系统需要写4个块。

 

  partial page write部分页写入问题:计算机硬件和操作系统,在极端情况下(如断电)往往并不能保证这一操作的原子性,16KB的数据,写入4K时,发生了系统断电或系统崩溃,只有一部分写是成功的。这时页数据出现不一样的情形,从而形成一个"断裂"的页,使数据产生混乱。

 

  doublewrite是在一个连续的存储空间,硬盘在写数据时是顺序写,而不是随机写。

 

  在数据库异常关闭的情况下启动时,都会做数据库恢复(redo)操作,恢复的过程中,数据库都会检查页面是不是合法(检验等等),若发现一个页面校验结果不一致,则此时会用到双写功能。

 

  若发生写失效,可以通过重做日志(Redo Log)进行恢复。重做日志中记录的是对页的物理操作,如偏移量800,写 'bbbb'记录,而不是页面的全量记录,若发生 partial page write (部分页写入)问题时,出现问题的是未修改过的数据,此时重做日志(Redo Log)无法处理。

 

2.2.3、InnoDB数据字典(Data Dictionary Header)

 

  向表中插入的记录称之为用户数据,MySQL作为一个软件来保管这些数据,提供方便的增删改查接口。当向一个表中插入一条记录时,MySQL先要校验一下插入语句对应的表存不存在,插入的列和表中的列是否符合,若语法没有问题,还需要知道该表的聚簇索引和所有二级索引对应的根页面是哪个表空间的哪个页面,再把记录插入对应索引的B+树中。

 

  MySQL除了保存插入的用户数据外,还需保存许多额外的信息,如:表属于哪个表空间,表里边有多少列,表对应的每一列的类型及表有多少索引。该表有哪些外键,外键对应哪个表的哪些列,某个表空间对应文件系统上文件路径是什么。

 

  上述数据并不是我们使用INSERT语句插入的用户数据,实际上是为了更好的管理用户数据而不得已引入的一些额外数据,这些数据也称为元数据。InnoDB存储引擎定义了一些列的内部系统表(internal system table)来记录这些元数据:

 

表名
描述
SYS_TABLES
整个InnoDB存储引擎中所有的表的信息
SYS_COLUMNS
整个InnoDB存储引擎中所有的列的信息
SYS_INDEXES
整个InnoDB存储引擎中所有的索引的信息
SYS_FIELDS
整个InnoDB存储引擎中所有的索引对应的列的信息
SYS_FOREIGN
整个InnoDB存储引擎中所有的外键的信息
SYS_FOREIGN_COLS
整个InnoDB存储引擎中所有的外键对应列的信息
SYS_TABLESPACES
整个InnoDB存储引擎中所有的表空间信息
SYS_DATAFILES
整个InnoDB存储引擎中所有的表空间对应文件系统的文件路径信息
SYS_VIRTUAL
整个InnoDB存储引擎中所有的虚拟生成列的信息

 

  这些系统表被称为数据字典,它们都是以B+树的形式保存在系统表空间的某些页面中,其中 SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FILEDS 这四个表称之为基本系统表。

 

  用户是不能直接访问InnoDB的这些内部系统表的,除非直接去解析系统表空间对应文件系统上的文件。

 

 

3、InnoDB的Buffer Pool

 

  Buffer Pool(缓冲池):InnoDB在访问表和索引数据时缓存的主内存区域,缓冲池允许直接从内存访问频繁使用的数据,这加快了处理速度。

 

  为了提高大容量读操作的效率,缓冲池被划分为可能包含多行的页面,为了提高缓存管理的效率,缓冲池被实现为页面链表;很少使用的数据使用最近最少使用(LRU)算法的变体从缓存中老化。

 

  了解如何利用缓冲池将频繁访问的数据保存在内存中是MySQL调优的一个重要方面。

 

3.1、缓存的重要性

 

  对于使用InnoDB作为存储引擎的表而言,不管是用于存储用户数据的索引(聚簇索引和二级索引),还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间不过是InnoDB对文件系统上一个或几个实际文件的抽象,数据仍存储在磁盘上。

 

  InnoDB存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,即使只需要访问一个页的一条记录,也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,如此将来有请求再次访问该页面时,就可以省去磁盘IO的开销。

 

3.2、Buffer Pool

 

  InnoDB为了缓存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存,这片内存叫做Buffer Pool(缓冲池)。默认情况下Buffer Pool只有8M大小,查看命令:

show variables like 'innodb_buffer_pool_size';

3.2.1、Buffer Pool内部组成

  Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小相同,都是16KB。InnoDB为每一个缓存页都创建了控制信息,控制信息包括该页所属的表空间编号、页号、缓存页在  Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息,及另外别的控制信息。

  每个缓存页对应的控制信息占用的内存大小是相同的,称为控制块。控制块和缓存页都是一一对应的,都被存放到Buffer Pool中,其中控制块被存放到Buffer Poold的前边,缓存页被存放到 Buffer Pool 后边,整个 Buffer Pool 对应的内存空间如下:

  0

  每个控制块大约占用缓存页大小的5%,设置的 innodb_buffer_pool_size 并不包含这部分控制块占用的内存空间大小,即InnoDB在为Buffer Pool向操作系统申请连续的内存空间时,这片连续的内存空间一般会比 innodb_pool_size 的值大5%左右。

3.2.2、free链表的管理

  启动MySQL服务器时,需要完成对 Buffer Pool 的初始化过程,先向操作系统申请 Buffer Pool 的内存空间,然后把它划分成若干对控制块和缓存页。此时并没有真实的磁盘页被缓存到 Buffer Pool 中(此时未用到),之后随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中。

  把所有空闲的缓存页对应的控制块作为一个节点放到链表中,这个链表可以被称作free链表,或者说空闲链表。刚完成初始化的Buffer Pool中所有的缓存页都是空闲的,所以每个缓存页对应的控制块都会被加入到free链表中,若该 Buffer Pool 中可容纳的缓存页数量为n,增加了free链表的效果如下:

 0

  有了此free链表后,每当需要从磁盘中加载一个页到Buffer Pool中时,从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(该页所在的表空间、页号等信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用。

缓存页的查找:

  访问某个页的数据时,会将该页从磁盘加载到Buffer Pool中,若该页已在Buffer Pool中则直接使用就可以了,如何判断该页是否在Buffer Pool中?

  根据表空间号+页号来定位一个页,即相当于表空间+页号是一个key,缓存页就是对应的value,如何通过一个key来快速找到一个value呢?

  表空间号 + 页号作为key,缓存页作为value创建一个哈希表,当需要访问某个页的数据时,先从哈希表中根据表空间号+页号查看是否对应的缓存页,若有,直接使用该缓存页;若没有,从free链表中选一个空闲的缓存页,再把磁盘中对应的页加载到该该缓存页的位置。

3.2.3、flush链表的管理

  当修改了Buffer Pool中某个缓存页的数据,则它就和磁盘上的页不一致了,这样的缓存页被称为脏页(dirty page)。每次修改缓存页后,并不会立即把修改同步到磁盘上,而是在未来的某个时间点进行同步,因为频繁的往磁盘中写数据会严重影响程序性能。

  这些脏页会被一个链表管理起来,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为该链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫 flush 链表。

3.2.4、LRU链表的管理

  Buffer Pool对应的内存大小有限,若缓存的页占用的内存大小超过了Buffer Pool大小,即free链表中没有多余的空闲缓存页时,需要把某些旧的缓存页从Buffer Pool中移除,再把新的页放入。思考:需要移除哪些旧的缓存页?

4.1、简单的LRU链表

  当Buffer Pool中不再有空闲的缓存页时,需要淘汰掉部分最近很少使用的缓存页,创建一个链表,该链表是为了按照最近最少使用的原则去淘汰缓存页的,该链表可以被称为LRU链表(LRU:Least Recently Used)。若要访问某个页,可以按如下步骤处理LRU链表:

  若该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页对应的控制块作为节点塞到LRU链表的头部;

  若该页已经缓存在Buffer Pool中,则直接把该页对一个的控制块移动到LRU链表的头部。

  只要使用到某个缓存页,就把该缓存页调整到LRU链表的头部,如此LRU链表尾部1就是最近最少会使用的缓存页,当Buffer Pool中的空闲缓存页使用完时,到LRU链表的尾部淘汰部分缓存页即可。

4.2、划分区域的LRU链表

  InnoDB提供了预读(readahead),即InnoDB认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到Buffer Pool中,根据触发方式的不同,预读可细分为如下两种

4.2.1、线性预读

  InnoDB提供了一个系统变量innodb_read_ahead_threshold,若顺序访问了某个区(extent)的页面超过这个系统变量的值,就会触发一次异步读取到下一个区中全部的页面到Buffer Pool的请求。

   

show variables like 'innodb_read_ahead_threshold';

  

  innodb_read_ahead_threshold系统变量的值默认是56,可以在服务器启动时通过启动参数或者服务器运行过程中直接调整该系统变量的值。

4.2.2、随机预读

  若Buffer Pool中已经缓存了某个区的13个连续的页面,不管这些页面是否顺序读取,都会触发一次异步读取本区中所有其他的页面到Buffer Pool的请求。

 

 

  

  若预读到Buffer Pool中的页成功的被使用到,就可以极大的提高语句执行的效率。若用不到,这些预读的页都会放到LRU链表的头部,若此时Buffer Pool的容量不大且很多预读的页面都未用到,就会导致处在LRU链表尾部的一些缓存页很快的被淘汰掉,会大大降低花奴才能命中率。

  InnoDB同时提供了innodb_random_read_ahead系统变量,默认值为OFF。

show variables like 'innodb_random_read_ahead';

   加载到Buffer Pool中的页不一定会被用到;若非常多使用频率偏低的页被同时加载到Buffer Pool时,可能会把使用频率高的页从Buffer Pool中淘汰掉,因为这两种情况的存在,InnoDB把这个LRU链表按照一定比例分成两截。

  存储使用频率高的缓存页的链表,称为热数据或者young区域;存储使用频率低的缓存页,称为冷数据或者old区域。

  对于InnoDB存储引擎,可以通过查看全局系统变量 innodb_old_blocks_pct 的值来确定old区域在LRU链表中所占的比例,如:

SHOW VARIABLES LIKE 'innodb_old_blocks_pct';

  

  默认情况下,old区域在LRU链表中所占的比例是37%,即old区域大约占LRU链表的3/8。

针对预读的页面可能不进行后续访问情况的优化:

  InnoDB规定,当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。如此针对预读到Buffer Pool进行后续访问的页面就会逐渐从old区域逐出,不会影响yong区域中被使用比较频繁的缓存页。

针对全表扫描时,短时间内访问大量使用频率非常低的页面情况优化:

  在进行全表扫描时,虽然首次被加载到Buffer Pool的页被放到了old区域的头部,但很快会被访问到,每次进行访问时又会把该页放到young区域的头部,这样仍然会把那些使用频率比较高的页面给替换下去。

  全表扫描的执行效率很低,出现全表扫描的语句需要尽快优化。而且在执行全表扫描的过程中,即使某个页面中有很多条记录,也就是去多次访问这个页面所花费的时间极少。

  在对某个old区域的缓存页进行第一次访问时就在它对应的控制块中记录下这个访问时间,若后续的访问时间与第一次的访问在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。时间间隔由系统标量innodb_old_blocks_time控制的:

 

 

SHOW VARIABLES LIKE 'innodb_old_blocks_time';

 

  

  默认值是1000,单位是毫秒,意味着从磁盘上被加载到LRU链表的old区域的某个页而言,若第一次和最后一次访问该页面的事件间隔小于1s,则该页不会被加入到young区域。若把 innodb_old_blocks_time的值设置为0,则每次访问一个页面就会把该页面放到young区域的头部。

  将LRU链表划分为young和old区域这两个部分,又添加了innodb_old_blocks_time这个系统变量,才使得预读机制和全表扫描造成的缓存命中率降低的问题得到了遏制,因为用不到的预读页面以及全表扫描的页面都只会被放到old区域,而不影响young区域中的缓存页。

进一步优化的LRU链表:

  在young区域的缓存页都是热点数据,即是可能被经常访问的,频繁的对LRU链表进行节点移动操作拖慢速度,为解决该问题,MySQL有很多优化策略,如只有被访问的缓存页位于young区域的1/4的后边,才会被移动到LRU链表头部,如此可以降低调整LRU链表的频率,从而提升性能。

3.3、多个Buffer Pool实例

  Buffer Pool本质上是InnoDB向操作系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool中的各种链表都需要加锁处理。在Buffer Pool特别大时,将其拆分成若干个小的Buffer Pool,每个Buffer Pool都称为一个实例,它们是独立的,独立的区申请内存空间,独立的管理各种链表,在多线程并发访问时不会相互影响,从而提高并发处理能力。

  在服务器启动的时候通过设置innodb_buffer_pool_instances的值来修改Buffer Pool实例的个数。

  每个Buffer Pool实例实际占用内存空间 = innodb_buffer_pool_size/innodb_buffer_pool_instance,即总大小除以实例的个数,结果就是每个Buffer Pool实例占用的大小。

  InnoDB规定:当innodb_buffer_pool_size(默认128M)的值小于1G时设置多个实例无效,InnoDB会默认把innodb_buffer_pool_instances的值修改为1。所以Buffer Pool大于或等于1G时设置应该多个Buffer Pool实例。

3.4、innodb_buffer_pool_chunk_size

  MySQL5.7.5及以后的版本支持在服务器运行过程中调整Buffer Pool大小的功能,但每次要重新调整Buffer Pool大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的Buffer Pool中的内容复制到这一块新空间,该步骤非常耗时。所以MySQL决定不再一次性为某Buffer Pool实例向操作系统申请一大片连续的内存空间,而是以一个chunk为单位向操作系统申请空间。一个Buffer Pool实例是由若干个chunk组成的,一个chunk就代表一片连续的内存空间,包含若干缓存页与其对应的控制块:

  0

  在服务器运行期间调整Buffer Pool的大小时以chunk为单位增加或者删除内存空间,不需要重新向操作系统申请一片大的内存,然后进行缓存页的赋值。chunk的大小是在启动操作MySQL服务器时通过 innodb_buffer_pool_chunk_size启动参数指定的,默认值是134217728,也就是128M。

  innodb_buffer_pool_chunk_size的值只能在服务器启动时指定,在服务器运行过程中是不可以修改的。

SHOW VARIABLES LIKE 'innodb_buffer_pool_chunk_size';

 

  Buffer Pool的缓存页除了用来缓存磁盘上的页面外,还可以存储锁信息、自适应哈希索引等信息。

3.5、InnoDB的内存结构总结

  InnoDB的内存结构和磁盘存储结构图总结如下:

 0

  其中的Insert/Change Buffer主要是用于对二级索引的写入优化,Undo空间则是undo日志一般放在系统表空间,但是通过参数配置后,也可以用独立表空间存放,所以用虚线表示。

 

posted @ 2024-03-08 17:13  无虑的小猪  阅读(1394)  评论(0编辑  收藏  举报