目录

InnoDB 架构

Innodb Architecture

InnoDB是一种兼顾高可靠性和高性能的通用存储引擎。在 MySQL 8.0 中,InnoDB是默认的 MySQL 存储引擎。除非您配置了不同的默认存​​储引擎,否则发出 CREATE TABLE 不带 ENGINE 子句的语句会创建一个InnoDB表。

InnoDB 的主要优势

  • 其 DML 操作遵循 ACID 模型,具有提交、回滚和崩溃恢复功能的事务以保护用户数据。

  • 行级锁定和 Oracle 风格的一致性读取提高了多用户并发性和性能。

  • InnoDB表在磁盘上排列数据以优化基于主键的查询。每个 InnoDB表都有一个称为聚集索引的主键索引,它组织数据以最小化主键查找的 I/O。

  • 为了保持数据完整性,InnoDB支持 FOREIGN KEY约束。使用外键检查插入、更新和删除以确保它们不会导致相关表之间的不一致。

Innodb Architecture

构成InnoDB存储引擎架构的内存和磁盘结构

/images/mysql/innodb-architecture.png

InnoDB On-Disk Structures

Tables

Indexes

Clustered and Secondary Indexes

每个InnoDB表都有一个特殊的索引,称为聚集索引,用于存储行数据。通常,聚集索引与主键同义。为了从查询、插入和其他数据库操作中获得最佳性能,了解如何InnoDB使用聚集索引来优化常见的查找和 DML 操作非常重要。

  • PRIMARY KEY在表上 定义 a时,InnoDB将其用作聚集索引。应该为每个表定义一个主键。如果没有逻辑唯一且非空的列或列集来使用主键,请添加一个自动增量列。自动增量列值是唯一的,并在插入新行时自动添加。

  • 如果您没有PRIMARY KEY为表定义 a,InnoDB则使用第一个 UNIQUE索引,其中所有键列都定义为NOT NULL聚集索引。

  • 如果表没有索引PRIMARY KEY或没有合适 的UNIQUE索引,则InnoDB 生成一个隐藏的聚集索引 ,该索引以GEN_CLUST_INDEX包含行 ID 值的合成列命名。行按InnoDB分配的行 ID 排序。行 ID 是一个 6 字节的字段,随着新行的插入而单调增加。因此,按行 ID 排序的行在物理上是按插入顺序排列的。

聚集索引如何加速查询

通过聚集索引访问行很快,因为索引搜索直接指向包含行数据的页面。如果表很大,与使用与索引记录不同的页面存储行数据的存储组织相比,聚集索引架构通常会节省磁盘 I/O 操作。

二级索引与聚集索引的关系

聚集索引以外的索引称为二级索引。在InnoDB中,二级索引中的每条记录都包含该行的主键列,以及为二级索引指定的列。 InnoDB使用此主键值在聚集索引中搜索行。

如果主键长,二级索引占用的空间就更多,所以主键短是有利的。

InnoDB 索引的物理结构

除空间索引外,InnoDB 索引都是B-tree数据结构。空间索引使用 R-trees,这是用于索引多维数据的专用数据结构。索引记录存储在其 B 树或 R 树数据结构的叶页中。索引页的默认大小为 16KB。页面大小由 innodb_page_size MySQL 实例初始化时的设置决定。

当新记录插入InnoDB 聚集索引时, InnoDB尝试留出 1/16 的页面空闲以供将来插入和更新索引记录。如果索引记录按顺序(升序或降序)插入,则生成的索引页大约是 15/16 满。如果以随机顺序插入记录,则页面从 1/2 到 15/16 满。

InnoDB在创建或重建 B 树索引时执行批量加载。这种创建索引的方法称为排序索引构建。该 innodb_fill_factor 变量定义在排序索引构建期间填充的每个 B 树页面上的空间百分比,剩余空间保留用于未来索引增长。空间索引不支持排序索引构建。innodb_fill_factor 设置为 100 会保留聚集索引页中 1/16 的空间以供将来索引增长使用 。

如果索引页面的填充因子 InnoDB 低于 MERGE_THRESHOLD 50%(如果未指定,则默认为 50%),则 InnoDB 尝试收缩索引树以释放页面。该 MERGE_THRESHOLD 设置适用于 B-tree 和 R-tree 索引。

Sorted Index Builds

InnoDB 在创建或重建索引时执行批量加载,而不是一次插入一个索引记录。这种索引创建方法也称为排序索引构建。空间索引不支持排序索引构建。

索引构建分为三个阶段。在第一阶段, 扫描聚集索引,生成索引条目并添加到排序缓冲区。当排序缓冲区变满时,条目将被排序并写入临时中间文件。此过程也称为 “运行”。在第二阶段,将一个或多个运行写入临时中间文件,对文件中的所有条目执行合并排序。在第三个也是最后一个阶段,排序后的条目被插入到 B-tree中。

InnoDB Full-Text Indexes

全文索引在基于文本的列(CHAR、 VARCHAR或 TEXT列)上创建,以加快对这些列中包含的数据的查询和 DML 操作。

全文索引被定义为 CREATE TABLE 语句的一部分或使用 ALTER TABLE 或 CREATE INDEX 添加到现有表中。

使用语法 MATCH() ... AGAINST 执行全文搜索。

Tablespaces 表空间

The System Tablespace 系统表空间

系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是 file-per-table 或通用表空间中创建的,它还可能包含表和索引数据。在以前的 MySQL 版本中,系统表空间包含InnoDB数据字典。在 MySQL 8.0 中,InnoDB将元数据存储在 MySQL 数据字典中。在以前的 MySQL 版本中,系统表空间还包含双写缓冲区存储区域。自 MySQL 8.0.20 起,此存储区域位于单独的双写文件中。

系统表空间可以有一个或多个数据文件。默认情况下, ibdata1在数据目录中创建一个名为 的系统表空间数据文件。系统表空间数据文件的大小和数量由innodb_data_file_path 启动选项定义。

File-Per-Table Tablespaces

file-per-table 表空间包含单个 InnoDB表的数据和索引,并存储在文件系统中的单个数据文件中。

File-Per-Table 表空间配置

InnoDB默认情况下在 file-per-table 表空间中创建表。

[mysqld]
innodb_file_per_table=ON
File-Per-Table 表空间数据文件

.ibd在 MySQL 数据目录下的模式目录 中的数据文件中创建一个 file-per-table 表空间 。该.ibd文件以表 ( table_name.ibd) 命名。

General Tablespaces 通用表空间

通用表空间InnoDB 是使用CREATE TABLESPACE语法创建的共享表空间。

1
2
3
4
5
6
CREATE TABLESPACE tablespace_name
    [ADD DATAFILE 'file_name']
    [FILE_BLOCK_SIZE = value]
        [ENGINE [=] engine_name]

CREATE TABLE t1 (c1 INT PRIMARY KEY) TABLESPACE ts1;

Undo Tablespaces 撤消表空间

撤消表空间包含撤消日志,它们是记录的集合,其中包含有关如何撤消事务对聚集索引记录的最新更改的信息。

MySQL 实例初始化时会创建两个默认的 undo 表空间。默认撤消表空间是在初始化时创建的,以便为在接受 SQL 语句之前必须存在的回滚段提供位置。至少需要两个撤消表空间来支持撤消表空间的自动截断。

Temporary Tablespaces 临时表空间

InnoDB使用会话临时表空间和全局临时表空间。

Doublewrite Buffer 双写缓冲区

双写缓冲区是一个存储区域,在 InnoDB将页面写入 InnoDB数据文件中的适当位置之前,将从缓冲池中刷新的页面写入其中。如果在页面写入过程中有操作系统、存储子系统或意外的mysqld 进程退出, InnoDB则可以在崩溃恢复期间从双写缓冲区中找到该页面的良好副本。

虽然数据被写入两次,但双写缓冲区不需要两倍的 I/O 开销或两倍的 I/O 操作。数据以大顺序块(chunk)的形式写入双写缓冲区,只需向操作系统调用一次 fsync()(innodb_flush_method 设置为 O_DIRECT_NO_FSYNC 的情况除外)。

在 MySQL 8.0.20 之前,双写缓冲存储区位于InnoDB系统表空间中。从 MySQL 8.0.20 开始,双写缓冲区存储区位于双写文件中。

Redo Log 重做日志

重做日志是一种基于磁盘的数据结构,用于在崩溃恢复期间纠正由不完整事务写入的数据。在正常操作期间,重做日志对由 SQL 语句或低级 API 调用产生的更改表数据的请求进行编码。在意外关闭之前未完成更新数据文件的修改会在初始化期间和接受连接之前自动重播。

默认情况下,重做日志在磁盘上由两个名为ib_logfile0和 的文件物理表示ib_logfile1。MySQL 以循环方式写入重做日志文件。重做日志中的数据根据​​受影响的记录进行编码;这些数据统称为重做。数据通过重做日志的过程由不断增加的LSN值表示。

Undo Logs 撤消日志

撤消日志是与单个读写事务关联的撤消日志记录的集合。撤消日志记录包含有关如何撤消事务对聚集索引 记录的最新更改的信息。如果另一个事务需要将原始数据视为一致读取操作的一部分,则从撤消日志记录中检索未修改的数据。撤消日志存在于 撤消日志段中,这些段包含在 回滚段中。回滚段驻留在 撤消表空间和全局临时表空间中。

驻留在全局临时表空间中的撤消日志用于修改用户定义临时表中数据的事务。这些撤消日志不是重做日志,因为它们不是崩溃恢复所必需的。它们仅用于在服务器运行时进行回滚。这种类型的撤消日志通过避免重做日志记录 I/O 来提高性能。

Row Format

MySQL 下用的比较多、比较广的存储引擎就属 InnoDB。InnoDB存储引擎下数据记录的存储格式——Row Format行格式

所谓Row Format行格式是指数据记录(或者称之为行)在磁盘中的物理存储方式。具体地,对于InnoDB存储引擎而言,常见的行格式类型有Compact、Redundant、Dynamic和Compressed

1
2
3
4
5
-- 创建数据表时,显示指定行格式
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称;
-- 创建数据表时,修改行格式
ALTER TABLE 表名 ROW_FORMAT=行格式名称;
SHOW TABLE STATUS FROM db_crm LIKE 't_customer';
SHOW [GLOBAL | SESSION] VARIABLES
    [LIKE 'pattern' | WHERE expr]
1
2
SHOW VARIABLES WHERE Variable_name LIKE'%binlog%'
SHOW VARIABLES LIKE '%binlog%'
Variable_name Value
binlog_format ROW
binlog_cache_size 32768
max_binlog_size 1073741824

Row

日志中会记录成每一行数据被修改的形式,然后在slave端再对相同的数据进行修改,只记录要修改的数据,只有value,不会有sql多表关联的情况。 优点:在row模式下,bin-log中可以不记录执行的sql语句的上下文相关的信息,仅仅只需要记录那一条记录被修改了,修改成什么样了,所以row的日志内容会非常清楚的记录下每一行数据修改的细节,非常容易理解。而且不会出现某些特定情况下的存储过程和function,以及trigger的调用和出发无法被正确复制问题。 缺点:在row模式下,所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容。

statement

每一条会修改数据的sql都会记录到master的binlog中,slave在复制的时候sql进程会解析成和原来master端执行多相同的sql再执行。 优点:在statement模式下首先就是解决了row模式的缺点,不需要记录每一行数据的变化减少了binlog日志量,节省了I/O以及存储资源,提高性能。因为他只需要记录在master上所执行的语句的细节以及执行语句的上下文信息。 缺点:在statement模式下,由于他是记录的执行语句,所以,为了让这些语句在slave端也能正确执行,那么他还必须记录每条语句在执行的时候的一些相关信息,也就是上下文信息,以保证所有语句在slave端被执行的时候能够得到和在master端执行时候相同的结果。另外就是,由于mysql现在发展比较快,很多的新功能不断的加入,使mysql的复制遇到了不小的挑战,自然复制的时候涉及到越复杂的内容,bug也就越容易出现。在statement中,目前已经发现不少情况会造成Mysql的复制出现问题,主要是修改数据的时候使用了某些特定的函数或者功能的时候会出现,比如:sleep()函数在有些版本中就不能被正确复制,在存储过程中使用了last_insert_id()函数,可能会使slave和master上得到不一致的id等等。由于row是基于每一行来记录的变化,所以不会出现,类似的问题。

Mixed

从官方文档中看到,之前的 MySQL 一直都只有基于 statement 的复制模式,直到 5.1.5 版本的 MySQL 才开始支持 row 复制。从 5.0 开始,MySQL 的复制已经解决了大量老版本中出现的无法正确复制的问题。但是由于存储过程的出现,给 MySQL Replication 又带来了更大的新挑战。另外,看到官方文档说,从 5.1.8 版本开始,MySQL 提供了除 Statement 和 Row 之外的第三种复制模式:Mixed,实际上就是前两种模式的结合。在 Mixed 模式下,MySQL 会根据执行的每一条具体的 SQL 语句来区分对待记录的日志形式,也就是在 statement 和 row 之间选择一种。新版本中的 statment 还是和以前一样,仅仅记录执行的语句。而新版本的 MySQL 中对 row 模式也被做了优化,并不是所有的修改都会以 row 模式来记录,比如遇到表结构变更的时候就会以 statement 模式来记录,如果 SQL 语句确实就是 update 或者 delete 等修改数据的语句,那么还是会记录所有行的变更。

采用链表的结构让数据页之间不需要是物理上的连续,而是逻辑上的连续。

如果通过 B+ 树的索引查询行记录,首先是从 B+ 树的根开始,逐层检索,直到找到叶子节点,也就是找到对应的数据页为止,将数据页加载到内存中,页目录中的槽(slot)采用二分查找的方式先找到一个粗略的记录分组,然后再在分组中通过链表遍历的方式查找记录。

术语 B-tree 的使用旨在作为索引设计的一般类别的参考。由于经典 B-tree 设计中不存在的复杂性,MySQL 存储引擎使用的 B-tree 结构可能被视为变体。 Reference Manual Glossary

此文暂时叫 B+树

只有叶子节点存数据

叶子节点双链表连接,支持范围查找

MongoDb indexes use a B-tree data structure. 这里应该也是 B+树

页结构的 Fil Header 中 FIL_PAGE_PREV并且 FIL_PAGE_NEXT是页面的“向后”和“向前”指针。此功能允许 InnoDB从一个叶子导航到另一个叶子,而无需回到根级别。这是您在经典 B-tree 中找不到的复杂性,这就是为什么InnoDB应该改为称为 B+-tree 的原因。

https://dev.mysql.com/doc/internals/en/innodb-record-structure.html

下一条记录指的并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定最小记录的下一条记录就本页中主键值最小的记录,而本页中主键值最大的记录的下一条记录就是最大记录 。

/images/mysql/1062096-20210509165726410-1510612182.png

Page Directory(页目录)

需要牢记的是,B+树索引本身并不能找到具体的一条记录,能找到只是该记录所在的页。数据库把页载入到内存,然后通过Page Directory再进行二叉查找,只不过二叉查找的时间复杂度很低,同时在内存中的查找很快,因此通常忽略这部分查找所用的时间。

/images/mysql/1062096-20210509172947411-376843780.png

FIle Trailer:

​ 为了检测页是否已经完整地写入磁盘(如可能发生的写入过程中磁盘损坏、机器关机等),InnoDB存储引擎的页中设置了File Trailer部分。

  • 数据页基于File Header中记录的上一页、下一页信息构成一个双向链表
  • 记录在数据页的User Records区按照主键大小排序储存,这样通过记录的next_record信息,数据页中的数据记录之间就形成了一条单向链表
  • 分组的时候,在组与组之间也是形成了一条单向链表

附录

https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html

https://dev.mysql.com/doc/internals/en/innodb.html

https://dev.mysql.com/doc/refman/8.0/en/innodb-file-space.html

https://dev.mysql.com/doc/refman/8.0/en/index-btree-hash.html

https://www.baeldung.com/cs/start-here