《MySQL 性能优化》之 InnoDB 存储引擎

作者: 不剪发的Tony老师
毕业于北京航空航天大学,十多年数据库管理与开发经验,目前在一家全球性的金融公司从事数据库架构设计。CSDN学院签约讲师以及GitChat专栏作者。csdn上的博客收藏于以下地址:https://tonydong.blog.csdn.net


文章目录

            InnoDB 概述
            InnoDB 系统结构
            InnoDB 内存结构
                缓冲池
                变更缓冲
                日志缓冲
                自适应哈希索引
            InnoDB 磁盘结构
                表空间
                表和索引
                双写缓冲
                重做日志
                回滚日志

上一篇我们介绍了 MySQL 服务器的体系结构,其中插件式存储引擎是 MySQL 与其他数据库管理系统的最大区别。InnoDB 作为 MySQL 默认的存储引擎应用最为广泛;因此,本篇我们来介绍一下 InnoDB 存储引擎。
InnoDB 概述

InnoDB 是一个具有高可靠性和高性能的通用存储引擎,也是 MySQL 5.5 之后的默认存储引擎。因此,如果CREATE TABLE语句没有指定ENGINE选项,默认创建的就是 InnoDB 表。

    📝使用SHOW VARIABLES LIKE 'default_storage_engine';命令可以查看默认的存储引擎。

在进一步讨论 InnoDB 体系结构之前,我们先介绍几个 InnoDB 存储引擎的关键特性:

    InnoDB 表的数据修改操作(DML)具有事务安全性(ACID),支持事务提交、事务回滚以及故障恢复,能够保障数据的一致性和完整性;
    InnoDB 采用更细粒度的行级锁和类似 Oracle 的一致性读(MVCC),能够提高并发性和性能。
    InnoDB 按照主键索引(clustered index)的顺序组织表中的数据,优化了基于主键字段的查询。
    InnoDB 支持外键约束(FOREIGN KEY),能够维护多个表之间的数据完整性。

当然,InnoDB 存储引擎提供的功能远远不止与此;正是由于这些强大的功能,使得 MySQL 能够像 Oracle、Microsoft SQL Server 等商业数据库一样大量应用在企业系统中。
InnoDB 系统结构

下图显示了 InnoDB 存储引擎的内存结构和磁盘结构。


记住这张图可以帮助我们理解 InnoDB 的体系结构,接下来我们分别讨论 InnoDB 的内存结构和磁盘结构。
InnoDB 内存结构

InnoDB 提供了自己的内存组件,主要包括缓冲池(Buffer Pool)、变更缓冲(Change Buffer)、日志缓冲(Log Buffer)以及自适应哈希索引(Adaptive Hash Index)技术。
缓冲池

缓冲池是 InnoDB 在内存中的一个缓冲区域,主要用于缓存访问过的表和索引等数据。缓冲池利用内存直接处理数据,避免磁盘操作,从而加快了数据处理的速度。

    📝在专用的 MySQL 服务器上,通常会给缓冲池分配多达 80% 的物理内存。

以下命令显示了 InnoDB 缓冲池相关的配置:

mysql> SHOW VARIABLES LIKE 'innodb_buffer_pool%';
+-------------------------------------+----------------+
| Variable_name                       | Value          |
+-------------------------------------+----------------+
| innodb_buffer_pool_chunk_size       | 8388608        |
| innodb_buffer_pool_dump_at_shutdown | ON             |
| innodb_buffer_pool_dump_now         | OFF            |
| innodb_buffer_pool_dump_pct         | 25             |
| innodb_buffer_pool_filename         | ib_buffer_pool |
| innodb_buffer_pool_in_core_file     | ON             |
| innodb_buffer_pool_instances        | 1              |
| innodb_buffer_pool_load_abort       | OFF            |
| innodb_buffer_pool_load_at_startup  | ON             |
| innodb_buffer_pool_load_now         | OFF            |
| innodb_buffer_pool_size             | 8388608        |
+-------------------------------------+----------------+
11 rows in set (0.00 sec)

 

其中,innodb_buffer_pool_chunk_size 表示每个缓冲块的大小;innodb_buffer_pool_instances 表示缓冲池的实例个数,每个实例由数量相同的缓冲块组成;innodb_buffer_pool_size 表示总的缓冲池大小,是 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances 的倍数。

缓冲池管理算法

为了提高大量读取操作时的效率,缓冲池被划分为页(page),每个页可能包含多行数据。为了提高缓存管理的效率,缓冲池被实现为页组成的链接列表。最终缓冲池使用特定的 LRU(最近最少使用)算法进行管理,从而将频繁访问的数据保留在缓存中,将最少使用的缓存页移除。

下图演示了缓冲池管理的 LRU 算法。


InnoDB 使用 LRU 算法略有改动,缓存池被分为两个部分:头部的 5/8 是最近被访问过的一个新的子列表,尾部的 3/8 是最近较少访问的一个旧的子列表。这个比例由系统变量 innodb_old_blocks_pct 控制:

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

 

当一个新的页需要被缓存时,最近最少使用的页将被剔除,新页将被放入缓存池的新旧子列表的中间;这种方式被称为中间点插入策略(midpoint insertion strategy)。用户提交的操作(例如 SQL 查询)或者 InnoDB 的预读(read-ahead)操作都会导致新页的缓存。

一方面,访问旧子列表中的页将会使得它被移动到新子列表的头部,变得更新。如果是用户操作引起的访问,该页将会立即被移动到新的子列表中;如果是预读操作引起的访问,不会立即导致移动,也可能根本不会移动。

另一方面,没有被访问的缓存页将会逐渐被移动到列表的尾部,变得更旧。新子列表和旧子列表中的页都会随着其他页的前移变得更旧;旧子列表中的页还会随着新页的加入变得更旧,最终到达列表的最尾部并且被剔除。

我们可以输入SHOW ENGINE INNODB STATUS命令,利用 InnoDB 标准监控输出查看缓冲池的使用指标,相关信息显示在 BUFFER POOL AND MEMORY 部分:

----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 8585216
Dictionary memory allocated 380485
Buffer pool size   512
Free buffers       249
Database pages     259
Old database pages 0
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 997, created 142, written 156
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: 259, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

 

其中的 Buffer pool size 是缓冲池分配的数据页数量(512),乘以 innodb_page_size(16384)等于缓冲池的大小(8388608)。

    📝关于 Buffer Pool 的配置和优化我们将会在 MySQL 实例优化的部分进行介绍。

变更缓冲

变更缓冲缓存了那些不在缓冲池中的二级索引(secondary index)页的修改操作。INSERT、UPDATE或者DELETE操作导致的变更将会在此缓冲,随后再合并(由其他读取操作引起)到缓冲池中。下图演示了变更缓冲的作用过程。


与聚集索引(clustered index)不同,二级索引通常是非唯一索引,索引的插入、更新、删除通常是顺序随机的操作。将变更进行缓存,并且在随后读入缓冲池时进行合并,能够避免将辅助索引页从磁盘读入缓冲池所需的大量随机 I/O。

当系统处于空闲状态或在缓慢关闭期间运行清除操作,定期将更新后的索引页写入磁盘。相对于每次将数据即写入磁盘,这种清除操作可以更有效地写入多个连续的索引值。

在内存中,变更缓冲属于缓冲池的一部分。在磁盘上,变更缓冲属于系统表空间的一部分;当数据库服务器关闭时,索引变更将会被缓冲到磁盘中。

系统变量 innodb_change_buffering 决定了何种类型的操作会被缓冲,默认为 ALL。

如果索引中包含降序索引列或主键中包含降序索引列,就不会对二级索引进行变更缓冲。

我们同样可以输入SHOW ENGINE INNODB STATUS命令,利用 InnoDB 标准监控输出查看变更缓冲的状态信息:

-------------------------------------
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 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 1 buffer(s)
Hash table size 2267, node heap has 3 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s

 

因为变更缓冲最开始只支持插入操作,所以显示为 INSERT BUFFER AND ADAPTIVE HASH INDEX。

    📝关于 Change Buffer 的配置和优化我们将会在 MySQL 实例优化的部分进行介绍。

日志缓冲

日志缓冲是重做日志(Redo Log)的内存缓冲,日志缓冲的大小由变量 innodb_log_buffer_size 决定,默认为 16 MB。日志缓冲的内容会定期刷新到磁盘文件。设置一个大的日志缓冲使得大型事务不必在提交之前将重做日志数据写入磁盘。因此,如果存在需要更新、插入或者删除大量数据的事务,可以通过增加日志缓冲的大小减少磁盘 I/O。

系统变量 innodb_flush_log_at_trx_commit 用于控制日志缓冲写入磁盘的方式。默认值为 1,即每次事务提交都会刷新缓冲到磁盘,满足 ACID 特性。

系统变量 innodb_flush_log_at_timeout 用于控制日志缓冲刷新到磁盘的频率。默认值为 1 秒,即每隔 1 秒刷新一次。

    📝关于 Log Buffer 的配置和优化我们将会在 MySQL 实例优化的部分进行介绍。

自适应哈希索引

InnoDB 包含了一个监控索引查找的机制,当 InnoDB 发现哈希索引可以提高查询的性能时会自动创建哈希索引。哈希索引基于索引键的一个前缀部分创建,可能只包含了 B+树索引中的一些值,通常时频繁访问的索引页。

当一个表能够差不多完全加载到内存中,哈希索引可以直接定位到所有数据,因此能够提高查询性能。自适应哈希索引特性由变量 innodb_adaptive_hash_index 设置,默认为 ON。但是由于它需要占用缓冲池的内存,只能用于等值查询,而且只在特定的情况下有效,因此 MySQL 5.6 开始建议关闭该选项。

我们可以利用SHOW ENGINE INNODB STATUS命令查看自适应哈希索引的使用情况,相关的数据也显示在 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分。
InnoDB 磁盘结构

InnoDB 提供的磁盘存储组件主要包括表空间(Tablespace)、表(Table)、索引(Index)、重做日志(Redo Log)、回滚日志(Undo Logs)以及双写缓冲(Doublewrite Buffer)。
表空间

表空间是一个逻辑上的存储概念,用于存储数据表、索引、回滚(Undo)数据等。一个表空间对应操作系统上的一个或者多个文件。从逻辑概念上来说,表空间又是由段(Segment)组成,段由区间(Extent)组成,区间由页(Page)组成,页最终由行(Row)组成。


一个 InndoDB 表通常对应一个数据段,而区间是磁盘分配的基本单位,页(默认为 16 KB)是 InndoDB 管理磁盘的最小单位,与操作系统的页(通常是 4 KB)概念不同。

InnoDB 提供的表空间包括:系统表空间(System Tablespace)、独立表空间(File-Per-Table Tablespaces)、通用表空间(General Tablespaces)、回滚表空间(Undo Tablespaces)以及临时表空间(Temporary Tablespaces)。

系统表空间

系统表空间用于存储双写缓冲和变更缓冲。如果创建表和索引时不使用独立表空间或通用表空间,它们也会被存储到系统表空间;不推荐这种做法。在 MySQL 8.0 之前,系统表空间中还包含了 InnoDB 数据字典信息;从 MySQL 8.0 开始, InnoDB 使用统一的 MySQL 数据字典存储元数据。

系统表空间可以拥有一个或多个数据文件。默认情况下在数据目录中创建一个名为 ibdata1 的系统表空间数据文件。系统表空间数据文件的大小和数量由系统参数 innodb_data_file_path 进行控制。

独立表空间

独立表空间(File-Per-Table Tablespaces)用于存储单个 InnoDB 表的数据和索引,每个表空间在文件系统中对应单个数据文件。举例来说,如果我们为 test 数据库创建一个表 t1,MySQL 会在数据目录下的 test 子目录中创建一个数据文件 t1.idb。

InnoDB 默认使用独立表空间创建表,可以使用系统变量 innodb_file_per_table 进行控制。如果禁用该参数,InnoDB 将会默认在系统表空间中创建表。

通用表空间

通用表空间是一种共享的 InnoDB 表空间,可以供多个表和索引使用。通用表空间比独立表空间具有更高的内存利用率。MySQL 服务器将会缓存表空间的元数据,包含多个表的通用表空间需要的内存比多个独立表空间更少。

通用表空间可以像独立表空间一样在 MySQL 数据目录内部或者外部创建数据文件,从而为关键的表指定单独的存储,例如 RAID 或者 DRBD,提高数据访问的性能。

通用表空间使用 CREATE TABLESPACE 语句创建。

回滚表空间

回滚表空间用于存储回滚日志,回滚日志记录中包含了撤销事务对聚集索引记录所作的最新修改所需的信息。回滚记录存储在回滚日志段中,回滚日志段存储在回滚段中。系统变量 innodb_rollback_segments 决定了每个回滚表空间分配的回滚段数量。

MySQL 实例初始化时会创建两个回滚表空间。 默认的回滚表空间在 innodb_undo_directory 参数指定的目录中创建,如果没有定义该参数,则在数据目录中创建。默认回滚表空间的数据文件名为 undo_001 和 undo_002,对应数据字典中的回滚表空间名为 innodb_undo_001 和 innodb_undo_002。

从 MySQL 8.0.14 开始,可以使用 CREATE UNDO TABLESPACE 增加额外的回滚表空间;一个 MySQL 实例最多可以存在 127 个回滚表空间,包括默认的两个回滚表空间。

临时表空间

InnoDB 存在两种临时表空间:会话临时表空间(session temporary tablespaces)和一个全局临时表空间(global temporary tablespace)。

会话临时表空间用于存储用户创建的临时表;当 InnoDB 被设置为磁盘内部临时表的存储引擎时,会话临时表空间也用于优化器创建的内部临时表。从 MySQL 8.0.16 开始,磁盘内部临时表的存储引擎永远都是 InnoDB;在此之前由参数 internal_tmp_disk_storage_engine 决定 。

系统变量 innodb_temp_tablespaces_dir 决定了会话临时表空间的文件目录,默认为数据目录下的 #innodb_temp 子目录。 表 INFORMATION_SCHEMA.INNODB_SESSION_TEMP_TABLESPACES 存储了会话临时表空间的元数据,表 INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO 存储了当前活动的用户临时表的元数据。

全局临时表空间(ibtmp1)存储了用户临时表修改信息的回滚段数据。系统变量 innodb_temp_data_file_path 定义了全局临时表空间数据文件的相对路径、名称、大小以及属性。如果没有指定该参数,默认在 innodb_data_home_dir 目录中创建一个名为 ibtmp1 的自动扩展的数据文件,初始大小略微大于 12 MB。
表和索引

表是数据库中存储数据的主要对象,使用CREATE TABLE语句创建。

CREATE TABLE t1 (a INT, b CHAR (20), PRIMARY KEY (a)) ENGINE=InnoDB;

 

其中,ENGINE 用于指定表的存储类型;如果不指定,MySQL 默认使用 InnoDB 存储引擎。使用以下命令查看表的信息:

mysql> SHOW TABLE STATUS LIKE 't%' \G
*************************** 1. row ***************************
           Name: t1
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 0
 Avg_row_length: 0
    Data_length: 16384
Max_data_length: 0
   Index_length: 0
      Data_free: 0
 Auto_increment: NULL
    Create_time: 2020-02-17 14:32:40
    Update_time: NULL
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options:
        Comment:
1 row in set (0.00 sec)

 

InnoDB 表按照索引的组织方式存储数据,被称为聚簇索引(clustered index)。具体来说,

    如果指定了 PRIMARY KEY,InnoDB 使用主键作为聚簇索引。推荐使用生成之后不会更改、不为空且不重复、经常作为查询条件的字段作为主键,例如各种编号。如果没有逻辑上唯一且非空的字段,可以使用自增字段 AUTO_INCREMENT 作为主键。
    如果没有定义 PRIMARY KEY,MySQL 使用第一个非空且唯一的索引字段作为 InnoDB 表的聚集索引。
    如果没有定义 PRIMARY KEY 也没有合适的 UNIQUE,InnoDB 会在内部生成一个 行 ID 字段,并且创建一个隐藏的聚集索引 GEN_CLUST_INDEX 。InnoDB 为表中的每一行生成一个递增的 ID 值,并且按照该顺序存储数据。

除了聚簇索引之外的索引被称为二级索引(secondary indexes)。InnoDB 二级索引中的每个索引记录都包含了主键索引列的值,以及二级索引的字段。InnoDB 使用主键值查找聚集索引中的数据行。因此,如果主键字段很长,二级索引就需要占用更多的磁盘空间,查找的效率就会更低。这也就是为什么 InnoDB 推荐使用简单的数字作为主键。

    📝关于索引和优化我们将会在 MySQL 模式优化的部分进行介绍。

双写缓冲

双写缓冲是系统表空间中的一个存储区域;在 InnoDB 将缓冲池刷新到数据文件之前,会先将缓冲页写入该区域。如果在写入数据页的过程中,出现了操作系统、存储系统或者 mysqld 进程崩溃,InnoDB 可以利用双写缓冲存储的缓冲页进行故障恢复。

由于 InnoDB 的数据页大小往往和操作系统数据页大小不一致,例如 InnoDB 为 16 KB,操作系统为 4 KB;此时 InnoDB 刷新一个数据页,操作系统需要刷新 4 个数据页,在系统故障时可能只刷新了部分数据页。双写缓冲会先把缓冲池的数据写入共享表空间,然后再刷新数据页;如果在这个过程中发生系统崩溃,InnoDB 可以从共享表空间获取到要刷新的数据,然后重新执行写入。

虽然数据需要写入两次,双写缓冲并不会导致两倍的 I/O 负载或者操作,因为双写缓冲只需要写入一个连续的数据块,只有一次 fsync() 系统调用。

双写缓冲由系统变量 innodb_doublewrite 控制,默认值为 ON。如果文件系统或者存储设备提供了防止部分写失效的功能,可以禁用双写缓冲。
重做日志

重做日志用于故障恢复时修复未完成事务的数据,它位于磁盘中,与内存中的日志缓冲相对应。在正常操作过程中,重做日志记录了表中的数据修改信息。当系统出现异常关闭后,重新启动时自动利用重做日志恢复未更新到数据文件中的修改。

默认情况下,重做日志物理上由两个文件 ib_logfile0 和 ib_logfile1 组成。MySQL 使用循环的方式写入重做日志文件。
回滚日志

回滚日志由一组回滚日志记录组成,这些记录属于单个读写事务。回滚日志记录包含了回滚一个事务对聚集索引记录的最新修改所需的信息。另外,如果另一个事务需要查看原始的数据(一致性读),将会从回滚日志记录中返回未修改前的数据。

回滚日志存储在回滚日志段中,后者包含在回滚段中;回滚段存储在回滚表空间以及全局临时表空间中。

下一篇我们将会讨论 MySQL 中的事务管理与并发控制,欢迎关注❤️、评论📝、点赞👍!