InnoDB 存储引擎介绍

1. MySQL 基础架构

前面写过几篇 MySQL 的文章,大多是对一些基础概念的讲解,当我想去了解存储引擎的时候发现不知从何下手,或者说不知道如何开头,回头想想好像对 MySQL 的基础架构还不是特别熟悉,所以本文尽管是介绍 InnoDB 存储引擎,但也会大致讲解一下 MySQL 的基础架构。

先看这样一张图(网上找的):

InnoDB 存储引擎介绍_第1张图片

可以看出 MySQL 最上层是连接组件。下面服务器是由连接池、管理工具和服务、SQL 接口、解析器、优化器、缓存、存储引擎、文件系统组成。

看的懂吗?看不懂。没事,咱翻译一下:

InnoDB 存储引擎介绍_第2张图片

大致上来说,MySQL 可以分为 Server层和 存储引擎层:

  • Server层: Server层涵盖了MySQL大部分核心业务功能,并且所有存储引擎的功能都在这一层实现,包括存储过程、触发器、视图等
  • 存储引擎层: 存储引擎有很多,包括 MyISAM、InnoDB 和 Memory 等,最常用的是 InnoDB,也是现在 MySQL 的默认存储引擎。

1.1 连接器

使用 MySQ L数据库,第一步是要连接MySQL数据库,这时候第一个迎接的你就是连接器。连接器负责跟客户端建立连接、获取权限、管理连接等工作。我们一般是使用命令mysql -uroot -p + Enter后输入密码并登录。

当输入密码提交登录时,MySQL客户端会与服务器建立连接,在完成TCP握手后,连接器就开始确认你所输入的用户名和密码。如果用户名密码正确则成功登录,如果用户名密码错误,会受到如下错误信息!

[root@VM-12-13-centos ~]# mysql -uroot -p
Enter password: 
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
[root@VM-12-13-centos ~]# 

1.2 查询缓存

连接建立完成后,假设你正在使用该 SQL 语句查询一条数据。

select * from user where id = 1

接下来 MySQL 执行逻辑就回到了查询缓存中。此时MySQL拿到一个查询请求后,先到查询缓存里看看是否执行过一这条 SQL 语句,在之前如果执行过这条语句,其结果大概就是以 Key-Value(键值对)的形式直接缓存在内存中。这里的 Key 代指的是查询语句,Value 代指的是查询结果。如果你所查询的语句在查询缓存中就命中缓存,它就会把该 SQL 语句对应的 Value 值结果集返回,这样就并不会执行其他 MySQL 零部件了,大大提高了查询效率。

InnoDB 存储引擎介绍_第3张图片

但是往往利弊是同时存在的,查询缓存有着一个致命的缺点,那就是查询缓存失效十分频繁。这里所说的查询缓存失效是指的只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此可能你废了很大的劲把结果存起来,还没使用呢,就被一个更新全清空了!大家都知道数据的宝贵,由于这个致命的缺点,导致查询缓存在 MySQL 8.0版本的时候就被抛弃了,也就是说 MySQL8.0 版本彻底删除了查询缓存

1.3 分析器

如果没有命中缓存,那就必须执行 SQL 语句了。这时候你所写的查询语句就到了分析器,分析器先会对 SQL 语句进行词法分析,它会分析并识别你所输入的空格、字符串和关键字都在 MySQL 中代表了什么,比如首先它会识别出来 select 关键字、表名、列名和条件。识别了 SQL 语句的这些后,就到了语法分析的阶段,它会根据MySQL 的语句标准来检查你所输入的 SQL 语句是否符合标准。如果不符合标准就会报出一个You have an error in your SQL syntax的语法错误提示。

注意: 一般语法错误提示第一个你所需要关注的是紧接着use near的内容,因为它会告诉你哪个语法附近有错误!

1.4 优化器

查询优化器,SQL 语句在查询之前会使用查询优化器对查询进行优化。

能进到优化器优化环节的 SQL 语句,说明在分析器分析的时候没有出现任何错误。那么优化器对该 SQL 语句做了些什么呢?假如一个 SQL 语句中是有索引的,优化器会根据优化规则选择合适的索引。比如之前讲的联合索引关于最左前缀原则改变索引的顺序,查询优化器就会自动优化成最优的查询 SQL。

简单来说就是优化器会判断你使用了哪种索引,使用了何种连接,其作用就是确定效率最高的执行方案

1.5 执行器

通过分析器知道了做什么,通过优化器知道了怎么做,这就遇到了一个问题,谁来做?可想而知就是执行器开始执行 SQL 语句。开始执行的时候,要先判断一下你对表是否有执行查询的权限,如果没有就会报出错误的提示信息。如果有权限,就打开表继续执行。执行器会根据表的引擎来调用提供的引擎接口,开始执行。

以上内容来自:MySQL基础架构分析

2. 存储引擎

从体系结构图中可以发现,MySQL 数据库区别于其他数据库的最重要的一个特点就是其插件式的表存储引擎。

MySQL 插件式的存储引擎架构提供了一系列标准的管理和服务支持,这些标准与存储引擎本身无关,可能是每个数据库系统本 身都必需的,如 SQL 分析器和优化器等,而存储引擎是底层物理结构和实际文件读写的实现,每个存储引擎开发者可以按照自己的意愿来进行开发。

需要特别注意的是,存储引擎是基于表的,而不是数据库

插件式存储引擎的好处是,每个存储引擎都有各自的特点,能够根据具体的应用建立不同存储引擎表。由于 MySQL 数据库的开源特性,用户可以根据 MySQL 预定义的存储引擎接口编写自己的存储引擎。若用户对某一种存储引擎的性能或功能不满意,可以通过修改源码来得到想要的特性,这就是开源带给我们的方便与力量。

由于 MySQL 数据库开源特性,存储引擎可以分为 MySQL 官方存储引擎和第三方存储引擎。有些第三方存储引擎很强大,如大名鼎鼎的 InnoDB 存储引擎(最早是第三方存储引擎,后被 Oracle 收购),其应用就极其广泛,甚至是 MySQL 数据库 OLTP(Online Transaction Processing 在线事务处理)应用中使用最广泛的存储引擎。

2.1 InnoDB

InnoDB 是 MySQL 的默认事务型引擎,也是最重要、使用最广泛的存储引擎。

它被设计用来处理大量的短期(short-lived)事务,短期事务大部分情况是正常提交的,很少会被回滚。InnoDB 的性能和自动崩溃恢复特性,使得它在非事务型存储的需求中也很流行。除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑 InnoDB 引擎。

如果要学习存储引擎,InnoDB 也是一个非常好的值得花最多的时间去深入学习的对象,收益肯定比将时间平均花在每个存储引擎的学习上要高得多。

2.2 MyISAM

在 MySQL 5.1 及之前的版本,MyISAM 是默认的存储引擎。MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但 MyISAM 不支持事务和行级锁,而且有一个毫无疑问的缺陷就是崩溃后无法安全恢复。

尽管 MyISAM 引擎不支持事务、不支持崩溃后的安全恢复,但它绝不是一无是处的。对于只读的数据,或者表比较小、可以忍受修复(repair)操作,则依然可以继续使用 MyISAM(但请不要默认使用 MyISAM,而是应当默认使用 InnoDB)。

但是 MyISAM 对整张表加锁,而不是针对行。读取时会对需要读到的所有表加共享锁,写入时则对表加排他锁,MyISAM 很容易因为表锁的问题导致典型的的性能问题。

2.3 Memory

如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没有关系,那么使用 Memory 表(以前也叫做 HEAP 表)是非常有用的。Memory 表至少比 MyISAM 表要快一个数量级,因为每个基于 MEMORY 存储引擎的表实际对应一个磁盘文件。该文件的文件名与表名相同,类型为 .frm 类型。该文件中只存储表的结构。而其数据文件,都是存储在内存中,这样有利于数据的快速处理,提高整个表的效率,不需要进行磁盘 I/O。所以 Memory 表的结构在重启以后还会保留,但数据会丢失。

Memroy 表在很多场景可以发挥好的作用:

  • 用于查找(lookup)或者映射(mapping)表,例如将邮编和州名映射的表;
  • 用于缓存周期性聚合数据(periodically aggregated data)的结果;
  • 用于保存数据分析中产生的中间数据。

Memory 表支持 Hash 索引,因此查找操作非常快。虽然 Memory 表的速度非常快,但还是无法取代传统的基于磁盘的表。Memroy 表是表级锁,因此并发写入的性能较低。它不支持 BLOB 或 TEXT 类型的列,并且每行的长度是固定的,所以即使指定了 VARCHAR 列,实际存储时也会转换成 CHAR,这可能导致部分内存的浪费。

除了以上介绍的 3 种,还有其他的如:Mrg_MyISAM ,Archive,Blackhole,CSV,Federated,还有第三方引擎XtraDB,TokuDB等等,这里就不一一介绍了。

2.4 对比

MyISAM 和 INNODB 的区别(重点):

  1. 事务安全(MyISAM不支持事务,INNODB支持事务);
  2. 外键 MyISAM 不支持外键, INNODB支持外键;
  3. 锁机制(MyISAM时表锁,innodb是行锁);
  4. 查询和添加速度(MyISAM批量插入速度快);
  5. 支持全文索引(MyISAM支持全文索引,INNODB不支持全文索引);
  6. MyISAM内存空间使用率比InnoDB低。

对于 Memory 存储,比如我们数据变化频繁,不需要入库,同时又频繁的查询和修改,我们考虑使用 Memory,速度极快。(如果 MySQL 重启的话,数据就不存在了)。

特点 MyISAM InnoDB Memory
事务 ✔️
锁机制 表锁 行锁 表锁
全文索引 ✔️
支持外键 ✔️
空间使用 -
批量插入速度

3. InnoDB 存储引擎

我们都知道,InnoDB 是 MySQL 默认的存储引擎,那么它对于数据时如何处理的呢?

InnoDB 是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。

而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB 存储引擎需要一条一条的把记录从磁盘上读出来吗?这势必会耗费大量的时间。

因此,InnoDB 采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB 中页的大小一般为 16KB(可以理解为 B+Tree 一个叶子节点存放的数据大小)。也就是在一般情况下,一次最少从磁盘中读取 16KB 的内容到内存中,一次最少把内存中的 16KB 内容刷新到磁盘中。

3.1 记录存储结构和索引页

我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式,也就是说页中保存着表中的一行行的数据。

3.1.1 行格式

InnoDB 存储引擎设计了 4 种不同类型的行格式,分别是 Compact、Redundant、Dynamic 和 Compressed 行格式。

我们可以通过以下命令查看默认情况下的行格式:

show table status like 'table'\G;

如:

mysql> show table status like 'biz_article'\G;
*************************** 1. row ***************************
           Name: biz_article
         Engine: InnoDB
        Version: 10
     Row_format: Compact
           Rows: 20
 Avg_row_length: 79462
    Data_length: 1589248
Max_data_length: 0
   Index_length: 81920
      Data_free: 4194304
 Auto_increment: 29
    Create_time: 2021-12-01 21:39:38
    Update_time: 2021-12-01 21:33:21
     Check_time: NULL
      Collation: utf8mb4_unicode_ci
       Checksum: NULL
 Create_options: row_format=COMPACT
        Comment: 
1 row in set (0.00 sec)

可以看到上表的默认行格式为 COMPACT。

COMPACT 的行记录存储方式如下:

InnoDB 存储引擎介绍_第4张图片

1、变长字段长度列表

我们知道 MySQL 支持一些变长的数据类型,比如 VARCHAR、VARBINARY、各种 TEXT 类型,各种 BLOB 类型,我们也可以把拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,所以我们 在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。

如果该可变字段允许存储的最大长度超过 255 字节并且真实存储的字节数超过 127 字节,则使用 2 个字节,否则使用 1 个字节。

数据溢出

另外,MySQL 规定一个表在声明时,所有 VARCHAR 类型的理论上长度总和不能超过 65535(还存在额外的开销,实际要小于这个值,为 65532,这里姑且认为上限为 65535)。如果超过 65535 并且没有将SQL_MODE设为严格模式,将会把varchar类型自动转为 text 类型。

# 非严格模式下会创建失败
mysql> create table test(a varchar(30000), b varchar(40000));
ERROR 1074 (42000): Column length too big for column 'a' (max = 16383); use BLOB or TEXT instead

2、NULL值列表

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

3、记录头信息

记录头信息是由固定的 5 个字节组成。5 个字节也就是 40 个二进制位,不同的位代表不同的意思。

InnoDB 存储引擎介绍_第5张图片

记录的真实数据除了我们自己定义的列的数据以外,MySQL 会为每个记录默认的添加一些列(也称为隐藏列)

InnoDB 存储引擎介绍_第6张图片

4、DB_ROW_ID(row_id)

非必须,6 字节,表示行 ID,唯一标识一条记录 。

5、DB_TRX_ID

必须,6 字节,表示事务 ID 。

6、DB_ROLL_PTR

必须,7 字节,表示回滚指针 。

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

其中,DB_TRX_ID(也可以称为 trx_id) 和 DB_ROLL_PTR(也可以称为 roll_ptr) 这两个列是必有的,但是 row_id 是可选的(在没有自定义主键以及 Unique 键的情况下才会添加该列)。

3.1.2 数据页

前边我们简单提了一下页的概念,它是 InnoDB 管理存储空间的基本单位,一个页的大小一般是 16KB。InnoDB 为了不同的目的而设计了许多种不同类型的页,存放我们表中记录的那种类型的页自然也是其中的一员,官方称这种存放记录的页为索引(INDEX)页,不过要理解成数据页也没问题,毕竟存在着聚簇索引这种索引和数据混合的东西。

数据页结构

InnoDB 存储引擎介绍_第7张图片

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

  • File Header:文件头部 38 字节 页的一些通用信息 ;
  • Page Header:页面头部 56 字节,数据页专有的一些信息 ;
  • Infimum + Supremum:最小记录和最大记录 26 字节,两个虚拟的行记录;
  • User Records:用户记录大小不确定,实际存储的行记录内容 ;
  • Free Space:空闲空间,大小不确定,页中尚未使用的空间;
  • Page Directory:页面目录,大小不确定,页中的某些记录的相对位置;
  • File Trailer:文件尾部,8 字节,校验页是否完整 。

3.2 表空间

表空间是一个抽象的概念,对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为表名.ibd 的实际文件。大家可以把表空间想象成被切分为许许多多个页的池子,当我们想为某个表插入一条记录的时候,就从池子中捞出一个对应的页来把数据写进去。

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

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

表空间又分为独立表空间和系统表空间。

3.2.1 独立表空间结构

区(extent)

表空间中的页可以达到 2³²个页,实在是太多了,为了更好的管理这些页面,InnoDB 中还有一个区(英文名:extent)的概念。对于 16KB 的页来说,连续的 64 个页就是一个区,也就是说一个区默认占用 1MB 空间大小。

不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每 256 个区又被划分成一个组。第一个组最开始的 3 个页面的类型是固定的,用来登记整个表空间的一些整体属性以及本组所有的区被称为 FSP_HDR,也就是 extent 0 ~ extent 255 这 256 个区,整个表空间只有一个 FSP_HDR。

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

引入区的主要目的是什么?

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

我们介绍 B+树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的随机 I/O。

再一次强调,磁盘的速度和内存的速度差了好几个数量级,随机 I/O 是非常慢的,所以我们应该尽量 让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序 I/O。

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

段(segment)

我们提到的范围查询,其实是对 B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以 InnoDB 对 B+树的叶子节点和非叶 子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自 己独有的区。存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成 2 个段,一个叶子节点段,一个非叶子节点段。

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

InnoDB 存储引擎介绍_第8张图片

3.2.2 系统表空间

系统表空间的结构和独立表空间基本类似,只不过由于整个 MySQL 进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,所以会比独立表空间多出一些记录这些信息的页面,相当于是表空间之首,所以它的表空间 ID(Space ID)是 0。

系统表空间和独立表空间的前三个页面的类型是一致的,只是页号为 3~7 的页面是系统表空间特有的。分别包括:

页号 页面类型 英文描述 描述
3 SYS Insert Buffer Header 存储 Insert Buffer 的头部信息
4 INDEX Insert Buffer Root 存储 Insert Buffer 的根页面
5 TRX_SYS Transction System 事务系统的相关信息
6 SYS First Rollback Segment 第一个回滚段的页面
7 SYS Data Dictionary Header 数据字典头部信息

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

4. InnoDB 的三大特性

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

*4.1 双写缓冲区

双写缓冲区(DoubleWrite Buffer),也叫双写机制,它是一种特殊文件 flush 技术,带给 InnoDB 存储引擎的是数据页的可靠性。

它的作用是,在把页写到数据文件之前,InnoDB 先把它们写到一个叫 doublewrite buffer(双写缓冲区)的连续区域内,在写 doublewrite buffer 完成后,InnoDB 才会把页写到数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB 在稍后的恢复过程中在 doublewrite buffer 中找到完好的 page 副本用于恢复。

所以,虽然叫双写缓冲区,但是这个缓冲区不仅在内存中有,更多的是属于 MySQL 的系统表空间,属于磁盘文件的一部分。我们从以下问题去了解它。

1️⃣ 那为什么要双写缓冲区呢?

双写缓冲区要写磁盘,数据之后也是要写磁盘,为什么还要提前写个双写缓冲区?这样不是增加 IO?

2️⃣ 什么是partial page write ?

由于InnoDB和操作系统的页大小不一致,InnoDB页大小一般为 16k,操作系统页大小为 4k,导致 InnoDB 回写数据到操作系统中,一个页面需要写4次,写入过程出现问题无法保持原子性。而计算机硬件和操作系统,写的过程如果崩溃或者断电,可能导致只有一部分写回到操作系统文件系统中,整个数据只是部分成功,其实数据是损坏的。这种情况下会产生 partial page write(部分页写入)问题。这时页数据出现不一样的情形,从而形成一个"断裂"的页,使数据产生混乱。

双写缓冲区是 InnoDB 在表空间上的 128 个页(2 个区,extend1 和 extend2),大小是 2MB。为了解决部分页写入问题,当 MySQL 将脏数据 flush 到数据文件的时候,先使将脏数据复制到内存中的一个区域(也是 2M),之后通过这个内存区域再分 2 次,每次写入 1MB 到系统表空间,然后马上调用 fsync 函数(fsync 是底层操作系统对外提供的一个 API 接口,它能保证数据必然已经落盘),同步到磁盘上。

在这个过程中是顺序写,开销并不大,在完成 doublewrite 写入后,再将数据写入各数据文件文件,这时是离散写入。所以在正常的情况下,MySQL 写数据页时,会写两遍到磁盘上,第一遍是写到 doublewrite buffer,第二遍是写到真正的数据文件中。如果发生了极端情况(断电),InnoDB 再次启动后,发现了一个页数据已经损坏,那么此时就可以从双写缓冲区中进行数据恢复了。

3️⃣ 是否一定需要double write?

前面说过,位于系统表空间上的 doublewrite buffer 实际上也是一个文件,写系统表空间会导致系统有更多的 fsync 操作,而硬盘的 fsync 性能因素会降低 MySQL 的整体性能。不过在存储上,doublewrite 是在一个连续的存储空间, 所以硬盘在写数据的时候是顺序写,而不是随机写,这样性能影响不大,相比不双写,降低了大概 5-10%左右。

所以,在一些情况下可以关闭 doublewrite 以获取更高的性能。比如在 slave 上可以关闭,因为即使出现了 partial page write 问题,数据还是可以从中继日志中恢复。比如某些文件系统 ZFS 本身有些文件系统本身就提供了部分写失效的防范机制,也可以关闭。

4️⃣ 双写缓冲区是如何恢复数据的?

双写缓冲区存在的目的就是 Innodb 为了保证 MySQL 数据的原子性。在数据库异常关闭的情况下启动时,都会做数据库恢复(redo)操作,恢复的过程中,数据库都会检查页面是不是合法(校验等等),如果发现一个页面校验结果不一致,则此时会用到双写这个功能,并对有问题的页进行数据恢复。

此时问题来了,既然 MySQL 写数据时异常,那么:

5️⃣ 假如写双写缓冲区挂了呢?

既然 MySQL 的双写缓冲区是双写机制,第一步写到磁盘的双写缓冲区自然有可能失败。假如失败了,MySQL 会根据磁盘上 B+ 树的结构,再配合上 Redo 日志对数据进行恢复。

问题又来了:

6️⃣ 既然可以通过 Redo 日志进行恢复,那么为什么还要双写缓冲区?

Redo 日志记录的是数据页的物理操作:对 XXX 表空间中的 XXX 数据页 XXX 偏移量的地方做了 XXX 更新。

如果页都损坏了,是无法进行任何恢复操作的。所以我们需要页的一个副本,如果服务器宕机了,可以通过副本把原来的数据页还原回来。这就是 doublewrite 的作用。

7️⃣ 那如果 Redo 日志失败了呢?

InnoDB 是存在事务的,因此不存在这种情况,每个事务在提交时都会将对应的 Redo 日志刷盘,只有 Redo 日志刷盘成功了,事务才能算完成。

8️⃣ 什么是脏页?

脏页,也叫内存页。当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为脏页。

干净页,内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,则称为干净页。

InnoDB 存储引擎介绍_第9张图片

4.2 Buffer Pool缓冲池

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

但是磁盘的速度慢,所以 InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。

将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO 的开销了。

注:Buffer Pool 和 MySQL 的查询缓存不是一个东西,Buffer Pool位于存储引擎层。

1️⃣ 什么是 Buffer Pool?

InnoDB 为了缓存磁盘中的页,在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做 Buffer Pool。

默认情况下 Buffer Pool 只有 128M 大小(这个值其实是偏小的)。

启动服务器的时候可以通过配置 innodb_buffer_pool_size 参数的值来控制 Buffer Pool 大小,

2️⃣ Buffer Pool 内部组成

Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB。

为了更好的管理这些在 Buffer Pool 中的缓存页,InnoDB 为每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、 缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息,当然还有一些别的控制信息,我们称之为控制块。

PS:MySQL 大部分缓存的数据都在 Buffer Pool 中,缓存页就是数据,数据库查询出来的数据都会缓存到缓存页中,便于下次快速查询。控制块保存了缓存也的各种信息地址等,用来找缓存页。

InnoDB 存储引擎介绍_第10张图片

3️⃣ Buffer Pool 中的链表结构

① Free 链表

Free 链表说简单些,就是将所有空闲的缓存页串起来,其实就是把空闲的缓存页对应的控制块的地址用一个链表来维护。这样下次有新数据进来,可以直接来 Free 链上直接找到空闲的缓存页。

Free 链还会有一个块用于存储,链表头,链表尾,以及链表上的数量。

InnoDB 存储引擎介绍_第11张图片

② Flush 链表

Flush 链的结构与 Free 链完全一样。

既然我们知道 Buffer Pool 缓存的是数据库中查询出来的数据,那么必然会存在一个问题,假如数据被修改了怎么办?

因此 MySQL 把缓冲区上面这种被修改过的数据的控制块也用一个链表进行维护,以此来快速定位被修改过的数据,也被称为脏数据(未落盘的数据),因此 Flush 链又被称为脏链。

PS:MySQL 数据提交后,并不是立刻落盘的,而是依然在缓冲区里,最后会统一落盘。既然数据提交了自然会有 Redo 日志,假如数据库挂了,数据也是能恢复的。

既然是脏数据为什么不知道把数据删掉?

因为 MySQL 本身数据并不是立刻落盘的。Flush 链上的数据,MySQL 会有定时任务去定时落盘。其次虽然是叫脏数据,但是本质上这些数据就是用户提交的数据,只是没落盘而已,读取时候直接读取是没有问题的。

③ LRU 链表

LRU(Least Recently Used) 链表是一种最近最少使用淘汰链表,简单逻辑就是维护一个链表,假如这个数据使用了就提到链表头。假如链表满了,需要淘汰,就从链表尾淘汰。

image-20211207191814712

而 MySQL 对 LRU 链表做了自己的优化改进

既然知道 MySQL 在缓冲区中缓存了查询的数据,但是查询的数据那么多,内存肯定放不下咋办?MySQL 就将数据用一个 LRU 链表进行维护,用来淘汰不常使用的数据,剩下的就是热门数据。

MySQL 对 LRU 改进措施:

  1. 将 LRU 链表分为两部分,前面为热数据去(Young 区),后面为冷数据区(Old 区),Old 区大小占 37%。
    优点:冷热链的切分,排除了全表扫描等类似的大数据量查询,直接把热门数据淘汰出缓冲区的情况。
  2. 对冷链数据移动到热链上做了时间限定。限定时间内对冷链上数据的访问不会移动到热数据区,只有超过这个时间,再次访问冷链上的数据,才会被移动到热数据区。
    优点:避免了短时间内全表扫描等大数据量频繁刷新导致,热门数据被移出热链的情况。

热链的部分,并非每次访问都会向前移动。只有在热点的后 1/4 内的数据,在访问时才会移动到热链头部,减少移动带来的资源消耗,提升性能。

可通过以下参数调整冷热区域占比占:

参数 说明
innodb_old_blocks_pct 调整冷热区域占比占比,默认 37%
innodb_old_blocks_time 调整限定间隔时间,默认 1 s

查看参数默认值:

mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37    |
+-----------------------+-------+
1 row in set (0.00 sec)

mysql> show variables like 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000  |
+------------------------+-------+
1 row in set (0.00 sec)

4️⃣ MySQL 刷新数据落盘的途径

MySQL 后台会有个定时器,定时将数据进行统一刷新落盘,以不影响用户线程处理正常的请求。主要有两种刷新路径:

  1. 从 LRU 链表的冷数据中刷新一部分页面到磁盘。

    后台线程会定时从 LRU 链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 innodb_lru_scan_depth 来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为 BUF_FLUSH_LRU

  2. 从 flush 链表中刷新一部分页面到磁盘。

    后台线程也会定时从 flush 链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为 BUF_FLUSH_LIST

有时候后台线程刷新脏页落盘的进度比较慢,导致用户线程在准备加载一个磁盘页到 Buffer Pool 时没有可用的缓存页,这时就会尝试看看 LRU 链表尾部有没有可以直接释放掉的未修改页面,如果没有的话会不得不将 LRU 链表尾部的一个脏页同步刷新到磁盘(和磁盘交互是很慢的,这会降低处理用户请求的速度)。这种刷新单个页面到磁盘中的刷新方式被称之为 BUF_FLUSH_SINGLE_PAGE

当然,有时候系统特别繁忙时,也可能出现用户线程批量的从 flush 链表中 刷新脏页的情况,很显然在处理用户请求过程中去刷新脏页是一种严重降低处理速度的行为,这属于一种迫不得已的情况。

5️⃣ 多个 Buffer Pool 实例

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

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

那每个 Buffer Pool 实例实际占多少内存空间呢?其实使用这个公式算出来的:

innodb_buffer_pool_size/innodb_buffer_pool_instances

也就是总共的大小除以实例的个数,结果就是每个 Buffer Pool 实例占用的大小。不过也不是说 Buffer Pool 实例创建的越多越好,分别管理各个 Buffer Pool 也是需要性能开销的,InnoDB 规定:

当 innodb_buffer_pool_size(默认 128M)的值小于 1G 的时候设置多个实例是无效的,InnoDB 会默认把 innodb_buffer_pool_instances 的值修改为 1。

所以 Buffer Pool 大于或等于 1G 的 时候设置应该多个 Buffer Pool 实例。

6️⃣ 查看 Buffer Pool 的状态信息

MySQL 给我们提供了 SHOW ENGINE INNODB STATUS\G 语句来查看关于 InnoDB 存储引擎运行过程中的一些状态信息,其中就包括 Buffer Pool 的一些信息。

截取Buffer Pool 的状态信息:

mysql> SHOW ENGINE INNODB STATUS\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
2021-12-07 19:46:57 0x7f25c4111700 INNODB MONITOR OUTPUT
=====================================

----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 208861
Buffer pool size   8191
Free buffers       7016
Database pages     1172
Old database pages 451
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 992, created 180, written 10118
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1172, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------

了解一下各参数:

  • Total memory allocated:代表 Buffer Pool 向操作系统申请的连续内存空间大小,包括全部控制块、缓存页、以及碎片的大小。
  • Dictionary memory allocated:为数据字典信息分配的内存空间大小,注意这个内存空间和 Buffer Pool 没啥关系,不包括在 Total memory allocated 中。
  • Buffer pool size:代表该 Buffer Pool 可以容纳多少缓存页,注意,单位是页。
  • Free buffers:代表当前 Buffer Pool 还有多少空闲缓存页,也就是 free 链表中还有多少个节点。
  • Database pages:代表 LRU 链表中的页的数量,包含 young 和 old 两个区域的节点数量。
  • Old database pages:代表 LRU 链表 old 区域的节点数量。
  • Modified db pages:代表脏页数量,也就是 flush 链表中节点的数量。
  • Pending reads:正在等待从磁盘上加载到 Buffer Pool 中的页面数量。当准备从磁盘中加载某个页面时,会先为这个页面在 Buffer Pool 中分配一个缓存页以及它对应的控制块,然后把这个控制块添加到 LRU 的 old 区域的头部, 但是这个时候真正的磁盘页并没有被加载进来,Pending reads 的值会跟着加 1。
  • Pending writes LRU:即将从 LRU 链表中刷新到磁盘中的页面数量。
  • Pending writes flush list:即将从 flush 链表中刷新到磁盘中的页面数量。
  • Pending writes single page:即将以单个页面的形式刷新到磁盘中的页面数量。
  • Pages made young:代表 LRU 链表中曾经从 old 区域移动到 young 区域头部 的节点数量。
  • Page made not young:在将 innodb_old_blocks_time 设置的值大于 0 时,首次访问或者后续访问某个处在old区域的节点时由于不符合时间间隔的限制而不能将其移动到 young 区域头部时,Page made not young 的值会加 1。
  • youngs/s:代表每秒从 old 区域被移动到 young 区域头部的节点数量。
  • non-youngs/s:代表每秒由于不满足时间限制而不能从 old 区域移动到 young 区域头部的节点数量。
  • Pages read、created、written:代表读取,创建,写入了多少页。后边跟着读取、创建、写入的速率。
  • Buffer pool hit rate:表示在过去某段时间,平均访问 1000 次页面,有多少次该页面已经被缓存到 Buffer Pool 了。
  • young-making rate:表示在过去某段时间,平均访问 1000 次页面,有多少次访问使页面移动到 young 区域的头部了。
  • not (young-making rate):表示在过去某段时间,平均访问 1000 次页面,有多少次访问没有使页面移动到 young 区域的头部。
  • LRU len:代表 LRU 链表中节点的数量。
  • unzip_LRU:代表 unzip_LRU 链表中节点的数量。
  • I/O sum:最近 50s 读取磁盘页的总数。
  • I/O cur:现在正在读取的磁盘页数量。I/O unzip sum:最近 50s 解压的页面数量。
  • I/O unzip cur:正在解压的页面数量。

4.3 自适应 Hash 索引

自适应哈希索引是 Innodb 引擎自行研发的,用于提高 MySQL 的查询效率。

在 InnoDB 存储引擎内部自己去监控索引表,如果监控到某个索引经常用,那么就认为是热数据,然后内部自己创建一个 hash 索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。

创建以后,如果下次又查询到这个索引, 那么直接通过 hash 算法推导出记录的地址,直接一次就能查到数据,比重复去 B+tree 索引中查询三四次节点的效率高了不少。

MySQL 在哈希索引的设计上还采用了热点分散技术,这样的哈希索引在 MySQL 上默认是启动 8 个的,热点数据会分散到不同的哈希索引上,因此热数据访问时,能将请求分散到不同的哈希索引上,提高了并发访问的性能。

InnoDB 存储引擎介绍_第12张图片

注:对于自适应哈希索引仅是数据库自身创建并使用的,我们并不能对其进行干预

也可以通过命令 SHOW ENGINE INNODB STATUS\G 可以看到当前自适应哈希索引的使用状况。

-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 0 buffer(s)
Hash table size 34673, node heap has 0 buffer(s)
Hash table size 34673, node heap has 0 buffer(s)
Hash table size 34673, node heap has 0 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 0 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
---

5. InnoDB内存结构

InnoDB 存储引擎介绍_第13张图片

6. 总结

本文主要介绍了 MySQL 的基础架构以及 InnoDB 引擎的的三大特性,其中对于 InnoDB 的三大特性做了一个小小的总结,且部分内容转自 it_lihongmin的博客,主要目的是为了提升对 MySQL 的深度和记录。

其中在了解 InnoDB 引擎的过程中,看到了这样一个面试题:

MySQL单个实例 buffer 数据和磁盘数据是如何保证强一致性的?

相信看完本文之后你应该能有答案~~~

你可能感兴趣的:(MySQL,mysql,数据库)