InnoDB Storage Engine

Stone大约 206 分钟

InnoDB Storage Engine

注意:

此文档对应的 MySQL 版本为 8.0.32 社区版。

Overview of MySQL Storage Engine Architecture

存储引擎是处理不同表类型的 SQL 操作的 MySQL 组件。InnoDB 是默认和最通用的存储引擎,Oracle 建议使用该存储引擎创建表。(MySQL 8.0 中的 CREATE TABLE 语句默认创建 InnoDB 表)

MySQL Server 使用可插拔存储引擎架构,架构图如下:

MySQL Architecture with Pluggable Storage Engines

使用 SHOW ENGINES 命令可以查看 MySQL Server 支持的存储引擎。

[(none)]> SHOW ENGINES;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| ndbcluster         | NO      | Clustered, fault-tolerant tables                               | NULL         | NULL | NULL       |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| ndbinfo            | NO      | MySQL Cluster system information storage engine                | NULL         | NULL | NULL       |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
11 rows in set (0.00 sec)

可插拔存储引擎架构提供了一组在所有底层存储引擎中通用的标准管理和支持服务。存储引擎作为数据库服务器的组件,负责执行数据库的实际数据 I/O 操作。可以针对特定应用程序需求(如数据仓库、事务处理或高可用性)使用特定的存储引擎。

Introduction to InnoDB

InnoDB 是一个通用的存储引擎,平衡了高可靠性和高性能。在 MySQL 8.0 中,InnoDB 是默认的存储引擎,使用不带 ENGINE 子句的 CREATE TABLE 语句会创建 InnoDB 表。

InnoDB 的主要优势有:

  • DML 操作遵循 ACID 模型,事务具有提交、回滚和崩溃恢复功能来保护用户数据。参考 InnoDB and the ACID Model
  • 行级锁和一致性读提高多用户并发和性能。
  • 使用聚簇主键索引优化查询,减少查找的 I/O。
  • 支持外键约束。
FeatureSupport
B-tree indexesYes
Backup/point-in-time recovery (Implemented in the server, rather than in the storage engine.)Yes
Cluster database supportNo
Clustered indexesYes
Compressed dataYes
Data cachesYes
Encrypted dataYes (Implemented in the server via encryption functions; In MySQL 5.7 and later, data-at-rest encryption is supported.)
Foreign key supportYes
Full-text search indexesYes (Support for FULLTEXT indexes is available in MySQL 5.6 and later.)
Geospatial data type supportYes
Geospatial indexing supportYes (Support for geospatial indexing is available in MySQL 5.7 and later.)
Hash indexesNo (InnoDB utilizes hash indexes internally for its Adaptive Hash Index feature.)
Index cachesYes
Locking granularityRow
MVCCYes
Replication support (Implemented in the server, rather than in the storage engine.)Yes
Storage limits64TB
T-tree indexesNo
TransactionsYes
Update statistics for data dictionaryYes

Best Practices for InnoDB Tables

使用 InnoDB 表的最佳实践:

  • 为每个表指定一个主键,一般使用最常查询的字段或者自增字段。
  • 为关联表指定外键,提高联合查询性能,并保证参照完整性。
  • 关闭自动提交,手动批量提交以提高性能。
  • 使用 START TRANSACTION 和 COMMIT 语句手动开启和提交事务,避免大量小事务和单个大事务。
  • 不要使用 LOCK TABLES 语句。要获得对一组行的独占写入访问权限,使用 SELECT ... FOR UPDATE 语法仅锁定要更新的行。
  • 启用参数 innodb_file_per_table (默认开启)或使用常规表空间将表的数据和索引放入单独的文件而不是系统表空间中。
  • 评估是否需要压缩 InnoDB 表。
  • 参数 sql_mode 中需包含 NO_ENGINE_SUBSTITUTION 选项(默认包含),强制创建 InnoDB 表。

Verifying that InnoDB is the Default Storage Engine

使用 SHOW ENGINES 语句:

[(none)]> SHOW ENGINES;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| ndbcluster         | NO      | Clustered, fault-tolerant tables                               | NULL         | NULL | NULL       |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| ndbinfo            | NO      | MySQL Cluster system information storage engine                | NULL         | NULL | NULL       |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
11 rows in set (0.00 sec)

或者查询 INFORMATION_SCHEMA.ENGINES 表:

[(none)]> SELECT * FROM INFORMATION_SCHEMA.ENGINES;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| ENGINE             | SUPPORT | COMMENT                                                        | TRANSACTIONS | XA   | SAVEPOINTS |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| ndbcluster         | NO      | Clustered, fault-tolerant tables                               | NULL         | NULL | NULL       |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| ndbinfo            | NO      | MySQL Cluster system information storage engine                | NULL         | NULL | NULL       |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
11 rows in set (0.00 sec)

InnoDB and the ACID Model

ACID 模型是一组数据库设计原则,强调对业务数据和应用程序的可靠性。MySQL 的 InnoDB 存储引擎严格遵循ACID 模型,因此数据不会因系统崩溃和硬件故障等异常情况而损坏。

  • A:原子性
  • C:一致性
  • I:隔离性
  • D:持久性

原子性

与 InnoDB 事务相关,相关的 MySQL 功能包括:

  • 参数 autocommit
  • 语句 COMMIT
  • 语句 ROLLBACK

一致性

涉及 InnoDB 内部处理,系统崩溃后恢复数据。相关的 MySQL 功能包括:

  • InnoDB 双写缓冲区
  • InnoDB 崩溃恢复

隔离性

与 InnoDB 事务相关,特别是事务隔离级别。相关的 MySQL 功能包括:

  • 参数 autocommit
  • 事务隔离级别和 SET TRANSACTION 语句。
  • InnoDB 锁信息

持久性

与硬件交互相关,相关的 MySQL 功能包括:

  • InnoDB 双写缓冲区
  • 参数 innodb_flush_log_at_trx_commit
  • 参数 sync_binlog
  • 参数 innodb_file_per_table
  • 存储设备的写缓冲区
  • 存储设备中的电池备份缓存
  • 运行 MySQL 的操作系统
  • UPS 供电保障
  • 备份策略

InnoDB Multi-Versioning

InnoDB 是一个多版本存储引擎,将有关已更改行的旧版本的信息存储在 UNDO 表空间的回滚段中,以支持并发和回滚等事务功能。InnoDB 使用回滚段中的信息来执行事务回滚所需的撤消操作以及生成行的早期版本以实现一致读。

在内部,InnoDB 向存储在数据库中的每一行添加三个字段:

  • 6 字节的 DB_TRX_ID 字段,表示事务标识符。
  • 7 字节的 DB_ROLL_PTR 字段,表示回滚指针,指向写入回滚段的 UNDO 日志记录。
  • 6 字节的 DB_ROW_ID 字段,表示行 ID,随着新行的插入而单调增加。

回滚段中的 UNDO 日志分为:

  • 插入 UNDO 日志,仅在事务回滚时需要,并且可以在事务提交后立即丢弃。
  • 更新 UNDO 日志,用于一致性读,只有在不存在对应的一致性读事务后,才能丢弃。

建议定期提交事务,包括仅发出一致读的事务。否则,InnoDB 无法丢弃更新 UNDO 日志中的数据,导致回滚段越来越大,从而占满 UNDO 表空间。

回滚段中 UNDO 日志记录的物理大小通常小于相应的插入或更新行,可以使用此信息来计算回滚段所需的空间。

在 InnoDB 中,当使用 SQL 语句删除行时,不会立即从数据库中物理删除。InnoDB 仅在丢弃对应的更新 UNDO 日志记录时才物理删除相应的行及其索引。

InnoDB 多版本并发控制 (MVCC) 处理二级索引的方式与聚簇索引不同。聚簇索引中的记录将就地更新,其隐藏的系统列指向 UNDO 日志条目,从中可以重建早期版本的记录。与聚簇索引记录不同,二级索引记录不包含隐藏的系统列,也不会就地更新。

更新二级索引列时,旧的二级索引记录将被标记为删除,新记录插入,并且最终会清除带有删除标记的记录。当二级索引记录被标记为删除或二级索引页被较新的事务更新时,InnoDB 会在聚簇索引中查找数据库记录。在聚簇索引中,将检查记录的 DB_TRX_ID,如果在启动读取事务后修改了记录,则会从 UNDO 日志中检索记录的正确版本。

如果将二级索引记录标记为删除,或者二级索引页由较新的事务更新,则不使用覆盖索引技术。

但是如果启用了索引条件下推(ICP)优化,并且只能使用索引中的字段评估 WHERE 条件的某些部分,则 MySQL 仍将 WHERE 条件的这一部分下推到使用索引进行评估的存储引擎。如果未找到匹配的记录,则避免进行聚簇索引查找。如果找到匹配的记录,即使在带有删除标记的记录中,InnoDB 也会在聚簇索引中查找该记录。

InnoDB Architecture

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

InnoDB Architecture

InnoDB In-Memory Structures

Buffer Pool

缓冲池是用于缓存表和索引数据的内存区域。缓冲池允许直接从内存访问常用数据,从而加快处理速度。通常将多达 80% 的物理内存分配给缓冲池。

Buffer Pool LRU Algorithm

使用变种的 LRU 算法将缓冲池作为列表(List)进行管理。当需要空间将新页添加到缓冲池时,会收回最近最少使用的页,并在列表中点(Midpoint)添加一个新页。此策略将列表分为两个子列表:

  • 在头部,是最近访问的新页的子列表
  • 在尾部,是最近访问较少的旧页的子列表

Buffer Pool List

该算法将频繁使用的页保留在新子列表(New Sublist),旧子列表(Old Sublist)包含不太频繁使用的页,这些页可能会被驱逐。

默认情况下,算法按如下方式运行:

  • 缓冲池的 3/8 用于旧子列表。
  • 列表的中点(Midpoint)是新子列表的尾部(Tail)与旧子列表的头部(Head)相交的边界。
  • 当 InnoDB 将一个页读取到缓冲池时,最初会将其插入中点(旧子列表的头部)。
  • 访问旧子列表中的页会将其移动到新子列表的头部。
  • 缓冲池中未被访问的页会向列表的尾部移动。

InnoDB 标准监视器输出中 BUFFER POOL AND MEMORY 的部分是关于缓冲池 LRU 算法的操作信息。

Buffer Pool Configuration

可以配置缓冲池以提高性能:

  • 理想情况下,将缓冲池的大小设置为尽可能大的值。
  • 在有足够内存的 64 位系统上,可以将缓冲池拆分为多个部分,以最大程度地减少并发操作对内存结构的争用。
  • 可以将经常访问的数据保留在内存中。
  • 可以控制如何以及何时执行预读请求,以异步方式将页预取到缓冲池中。
  • 可以控制何时发生后台刷新,以及是否根据工作负载动态调整刷新速率。
  • 以配置 InnoDB 如何保留当前缓冲池状态,以避免服务器重新启动后长时间预热。

Monitoring the Buffer Pool Using the InnoDB Standard Monitor

使用 SHOW ENGINE INNODB STATUS 命令访问 InnoDB 标准监视器,查看缓冲池状态:

[(none)]> SHOW ENGINE INNODB STATUS\G
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Dictionary memory allocated 491793
Buffer pool size   65531
Free buffers       64257
Database pages     1269
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 1126, created 143, written 224
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: 1269, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

InnoDB 标准监视器输出中提供的每秒平均值基于自上次打印 InnoDB 标准监视器输出以来经过的时间。

各个指标的含义如下:

NameDescription
Total memory allocatedThe total memory allocated for the buffer pool in bytes.
Dictionary memory allocatedThe total memory allocated for the InnoDB data dictionary in bytes.
Buffer pool sizeThe total size in pages allocated to the buffer pool.
Free buffersThe total size in pages of the buffer pool free list.
Database pagesThe total size in pages of the buffer pool LRU list.
Old database pagesThe total size in pages of the buffer pool old LRU sublist.
Modified db pagesThe current number of pages modified in the buffer pool.
Pending readsThe number of buffer pool pages waiting to be read into the buffer pool.
Pending writes LRUThe number of old dirty pages within the buffer pool to be written from the bottom of the LRU list.
Pending writes flush listThe number of buffer pool pages to be flushed during checkpointing.
Pending writes single pageThe number of pending independent page writes within the buffer pool.
Pages made youngThe total number of pages made young in the buffer pool LRU list (moved to the head of sublist of “new” pages).
Pages made not youngThe total number of pages not made young in the buffer pool LRU list (pages that have remained in the “old” sublist without being made young).
youngs/sThe per second average of accesses to old pages in the buffer pool LRU list that have resulted in making pages young. See the notes that follow this table for more information.
non-youngs/sThe per second average of accesses to old pages in the buffer pool LRU list that have resulted in not making pages young. See the notes that follow this table for more information.
Pages readThe total number of pages read from the buffer pool.
Pages createdThe total number of pages created within the buffer pool.
Pages writtenThe total number of pages written from the buffer pool.
reads/sThe per second average number of buffer pool page reads per second.
creates/sThe average number of buffer pool pages created per second.
writes/sThe average number of buffer pool page writes per second.
Buffer pool hit rateThe buffer pool page hit rate for pages read from the buffer pool vs from disk storage.
young-making rateThe average hit rate at which page accesses have resulted in making pages young. See the notes that follow this table for more information.
not (young-making rate)The average hit rate at which page accesses have not resulted in making pages young. See the notes that follow this table for more information.
Pages read aheadThe per second average of read ahead operations.
Pages evicted without accessThe per second average of the pages evicted without being accessed from the buffer pool.
Random read aheadThe per second average of random read ahead operations.
LRU lenThe total size in pages of the buffer pool LRU list.
unzip_LRU lenThe length (in pages) of the buffer pool unzip_LRU list.
I/O sumThe total number of buffer pool LRU list pages accessed.
I/O curThe total number of buffer pool LRU list pages accessed in the current interval.
I/O unzip sumThe total number of buffer pool unzip_LRU list pages decompressed.
I/O unzip curThe total number of buffer pool unzip_LRU list pages decompressed in the current interval.

也可以使用 SHOW GLOBAL STATUS 或者通过表 INFORMATION_SCHEMA.INNODB_BUFFER_POOL_STATS 查看。

Change Buffer

写缓冲区是一种特殊的数据结构,当非唯一二级索引页不在缓冲池中时,会缓存对这些页所做的更改。缓冲更改(可能由 INSERT、UPDATE 或 DELETE 操作 (DML) 产生)在之后通过其他读取操作将页加载到缓冲池中时进行合并。避免了对非唯一二级索引页的随机 I/O 访问,提高了性能。

Change Buffer

Configuring Change Buffering

写缓冲适用于大数据量的 DML 操作,例如批量插入。

使用参数 innodb_change_buffering 指定写缓冲适用的 DML 操作,默认为 all

ValueNumeric ValueDescription
none0Do not buffer any operations.
inserts1Buffer insert operations.
deletes2Buffer delete marking operations; strictly speaking, the writes that mark index records for later deletion during a purge operation.
changes3Buffer inserts and delete-marking operations.
purges4Buffer the physical deletion operations that happen in the background.
all5The default. Buffer inserts, delete-marking operations, and purges.

Configuring the Change Buffer Maximum Size

使用动态参数 innodb_change_buffer_max_size 指定写缓冲占缓冲池的百分比,默认为 25,最大为 50。

[(none)]> SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_change_buffer_max_size | 25    |
+-------------------------------+-------+
1 row in set (0.00 sec)

如果有大量的插入,更新和删除操作,建议增大该参数。

Monitoring the Change Buffer

InnoDB 标准监视器输出包括写缓冲信息。

[(none)]> 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 276707, node heap has 0 buffer(s)
Hash table size 276707, node heap has 0 buffer(s)
Hash table size 276707, node heap has 0 buffer(s)
Hash table size 276707, node heap has 0 buffer(s)
Hash table size 276707, node heap has 4 buffer(s)
Hash table size 276707, node heap has 0 buffer(s)
Hash table size 276707, node heap has 0 buffer(s)
Hash table size 276707, node heap has 1 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s

查询表 INFORMATION_SCHEMA.INNODB_BUFFER_PAGE 获取缓冲池页信息,包括写缓冲索引页 IBUF_INDEX 和 写缓冲位图页 IBUF_BITMAP

例子:查看 IBUF_INDEXIBUF_BITMAP 页占缓冲池页的百分比:

[(none)]> SELECT (SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
    ->    WHERE PAGE_TYPE LIKE 'IBUF%') AS change_buffer_pages,
    ->    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE) AS total_pages,
    ->    (SELECT ((change_buffer_pages/total_pages)*100))
    ->    AS change_buffer_page_percentage;
+---------------------+-------------+-------------------------------+
| change_buffer_pages | total_pages | change_buffer_page_percentage |
+---------------------+-------------+-------------------------------+
|                   9 |       65530 |                        0.0137 |
+---------------------+-------------+-------------------------------+
1 row in set (0.68 sec)

注意:查询表 INFORMATION_SCHEMA.INNODB_BUFFER_PAGE 对性能有影响。

查询表 performance_schema.setup_instruments 获取写缓冲相关等待。

[(none)]> SELECT * FROM performance_schema.setup_instruments
    ->    WHERE NAME LIKE '%wait/synch/mutex/innodb/ibuf%';
+-------------------------------------------------------+---------+-------+------------+-------+------------+---------------+
| NAME                                                  | ENABLED | TIMED | PROPERTIES | FLAGS | VOLATILITY | DOCUMENTATION |
+-------------------------------------------------------+---------+-------+------------+-------+------------+---------------+
| wait/synch/mutex/innodb/ibuf_bitmap_mutex             | NO      | NO    |            | NULL  |          0 | NULL          |
| wait/synch/mutex/innodb/ibuf_mutex                    | NO      | NO    |            | NULL  |          0 | NULL          |
| wait/synch/mutex/innodb/ibuf_pessimistic_insert_mutex | NO      | NO    |            | NULL  |          0 | NULL          |
+-------------------------------------------------------+---------+-------+------------+-------+------------+---------------+
3 rows in set (0.02 sec)

Adaptive Hash Index

自适应哈希索引使 InnoDB 能够在具有适当工作负载和足够缓冲池内存的系统上像内存数据库一样高性能,而不会牺牲事务功能或可靠性。

使用参数 innodb_adaptive_hash_index 启用自适应哈希索引,默认为 ON

[(none)]> SHOW VARIABLES LIKE 'innodb_adaptive_hash_index%';
+----------------------------------+-------+
| Variable_name                    | Value |
+----------------------------------+-------+
| innodb_adaptive_hash_index       | ON    |
| innodb_adaptive_hash_index_parts | 8     |
+----------------------------------+-------+
2 rows in set (0.01 sec)

使用参数 innodb_adaptive_hash_index_parts 指定自适应哈希索引分区数量,默认为 8,最大为 512。

Log Buffer

日志缓冲区存放将要写入到磁盘日志文件的数据。使用参数 innodb_log_buffer_size 指定日志缓冲区大小,默认为 16MB。

[(none)]> SHOW VARIABLES LIKE 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
1 row in set (0.01 sec)

日志缓冲区的数据会被定期刷新到磁盘。大日志缓冲区可以使大事务在提交之前无需将重做日志数据写入磁盘。因此,如果有更新、插入或删除许多行的事务,则增加日志缓冲区的大小可以节省磁盘 I/O。

使用参数 innodb_flush_log_at_trx_commit 控制如何将日志缓冲区的数据写入和刷新到磁盘,默认为 1,表示每次事务提交就将数据写入和刷新到磁盘。

使用参数 innodb_flush_log_at_timeout 控制日志刷新频率,默认为 1 秒,只有在参数 innodb_flush_log_at_trx_commit 设置为 0 或者 2 时才起作用。

  • 设置为 0 时,重做日志数据还是在日志缓冲区,达到 innodb_flush_log_at_timeout 设置时,才会将数据写入和刷新到磁盘。
  • 设置为 2 时,每次事务提交就将数据写入磁盘(实际上是写入到操作系统的文件缓存),达到 innodb_flush_log_at_timeout 设置时,再刷新到磁盘。
[(none)]> SHOW VARIABLES LIKE 'innodb_flush_log_at%';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_timeout    | 1     |
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+
2 rows in set (0.00 sec)

InnoDB On-Disk Structures

Tables

Creating InnoDB Tables

使用 CREATE TABLE 语句创建 InnoDB 表:

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

如果 InnoDB 为默认存储引擎,则可以省略 ENGINE=InnoDB 子句。

[(none)]> SELECT @@default_storage_engine;
+--------------------------+
| @@default_storage_engine |
+--------------------------+
| InnoDB                   |
+--------------------------+
1 row in set (0.00 sec)

默认情况下,InnoDB 创建表于独立表空间(file-per-table tablespaces)。如果要在 InnoDB 系统表空间中创建表,则需要禁用参数 innodb_file_per_table。如果要在常规表空间中创建 InnoDB 表,使用 CREATE TABLE ... TABLESPACE 语法。

Row Formats

InnoDB 表的行格式决定了其行在磁盘上的物理存储方式。支持的行格式有:

  • REDUNDANT
  • COMPACT
  • DYNAMIC
  • COMPRESSED

DYNAMIC 为默认行格式。使用参数 innodb_default_row_format 指定默认行格式,也可以使用 CREATE TABLE 或者 ALTER TABLEROW_FORMAT 选项显示指定表的行格式。

[(none)]> SHOW VARIABLES LIKE 'innodb_default_row_format';
+---------------------------+---------+
| Variable_name             | Value   |
+---------------------------+---------+
| innodb_default_row_format | dynamic |
+---------------------------+---------+
1 row in set (0.04 sec)
Primary Keys

建议在创建表时为每个表定义一个主键。一般使用自增字段作为主键。

# The value of ID can act like a pointer between related items in different tables.
CREATE TABLE t5 (id INT AUTO_INCREMENT, b CHAR (20), PRIMARY KEY (id));

# The primary key can consist of more than one column. Any autoinc column must come first.
CREATE TABLE t6 (id INT AUTO_INCREMENT, a INT, b CHAR (20), PRIMARY KEY (id,a));
Viewing InnoDB Table Properties

使用 SHOW TABLE STATUS 语句查看 InnoDB 表的属性。

[(none)]> SHOW TABLE STATUS FROM menagerie LIKE 'pet'\G
*************************** 1. row ***************************
           Name: pet
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 8
 Avg_row_length: 2048
    Data_length: 16384
Max_data_length: 0
   Index_length: 0
      Data_free: 0
 Auto_increment: NULL
    Create_time: 2023-02-07 20:15:27
    Update_time: NULL
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options: 
        Comment: 
1 row in set (0.00 sec)

也可以查询 INFORMATION_SCHEMA.INNODB_TABLES 获取表属性。

[(none)]> SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME='menagerie/pet'\G
*************************** 1. row ***************************
          TABLE_ID: 1072
              NAME: menagerie/pet
              FLAG: 33
            N_COLS: 9
             SPACE: 10
        ROW_FORMAT: Dynamic
     ZIP_PAGE_SIZE: 0
        SPACE_TYPE: Single
      INSTANT_COLS: 0
TOTAL_ROW_VERSIONS: 0
1 row in set (0.04 sec)

Importing InnoDB Tables

使用传输表空间迁移数据。

为避免表名大小写在各个平台不兼容问题,建议在初始化服务器时设置参数:

[mysqld]
lower_case_table_names=1
Prerequisites
  • 指定参数 innodb_file_per_table 为启用,默认为启用。
  • 源环境和目标环境的参数 innodb_page_size 需要一致。
  • 如果表存在外键关系,在执行 DISCARD TABLESPACE 语句前,需要指定参数 foreign_key_checks 为禁用。此外,还应在同一逻辑时间点导出所有与外键相关的表,因为 ALTER TABLE ... IMPORT TABLESPACE 不对导入的数据强制实施外键约束。为此,需要停止更新相关表,提交所有事务,获取表上的共享锁,然后执行导出操作。
  • 源环境和目标环境的版本需要一致。
  • 源环境和目标环境的参数 innodb_default_row_format 需要一致。
Importing Tables

迁移非分区表的步骤:

  1. 在目标环境,创建与原环境相同的表,可以使用 SHOW CREATE TABLE 语句获取表定义。
mysql> USE test;
mysql> CREATE TABLE t1 (c1 INT) ENGINE=INNODB;
  1. 在目标环境,丢弃刚刚为表创建的表空间。
mysql> ALTER TABLE t1 DISCARD TABLESPACE;
  1. 在源环境,使用 FLUSH TABLES ... FOR EXPORT 语句静默要迁移的表,只允许在该表上进行只读事务。该语句运行时,InnoDB 在表的模式目录中生成一个 .cfg 元数据文件,该文件包含在导入操作期间用于模式验证的元数据。
mysql> USE test;
mysql> FLUSH TABLES t1 FOR EXPORT;
  1. 在源环境,将 .ibd 文件和 .cfg 元数据文件复制到目标环境,权限需要保持一致。如果目标环境是主从架构,则需要将文件复制到主库和从库。
$> scp /path/to/datadir/test/t1.{ibd,cfg} destination-server:/path/to/datadir/test

注意:如果是加密表空间,还需要拷贝 .cfp 文件。

  1. 在源环境,使用 UNLOCK TABLES 释放 FLUSH TABLES ... FOR EXPORT 语句获取的锁,并移除 .cfg 文件。
mysql> USE test;
mysql> UNLOCK TABLES;
  1. 在目标环境,导入表空间。
mysql> USE test;
mysql> ALTER TABLE t1 IMPORT TABLESPACE;
Importing Partitioned Tables

迁移分区表的步骤:

  1. 在目标环境,创建与原环境相同的分区表,可以使用 SHOW CREATE TABLE 语句获取表定义。
mysql> USE test;
mysql> CREATE TABLE t1 (i int) ENGINE = InnoDB PARTITION BY KEY (i) PARTITIONS 3;

在对应的模式目录下,可以看到每个分区对应的 .ibd 文件。

mysql> \! ls /path/to/datadir/test/
t1#p#p0.ibd  t1#p#p1.ibd  t1#p#p2.ibd
  1. 在目标环境,丢弃刚刚为分区表创建的表空间。
mysql> ALTER TABLE t1 DISCARD TABLESPACE;
  1. 在源环境,使用 FLUSH TABLES ... FOR EXPORT 语句静默要迁移的分区表,只允许在该表上进行只读事务。该语句运行时,InnoDB 在表的模式目录中为每个表空间文件生成 .cfg 元数据文件,该文件包含在导入操作期间用于模式验证的元数据。
mysql> USE test;
mysql> FLUSH TABLES t1 FOR EXPORT;

mysql> \! ls /path/to/datadir/test/
t1#p#p0.ibd  t1#p#p1.ibd  t1#p#p2.ibd
t1#p#p0.cfg  t1#p#p1.cfg  t1#p#p2.cfg
  1. 在源环境,将 .ibd 文件和 .cfg 元数据文件复制到目标环境,权限需要保持一致。如果目标环境是主从架构,则需要将文件复制到主库和从库。
$>scp /path/to/datadir/test/t1*.{ibd,cfg} destination-server:/path/to/datadir/test

注意:如果是加密表空间,还需要拷贝 .cfp 文件。

  1. 在源环境,使用 UNLOCK TABLES 释放 FLUSH TABLES ... FOR EXPORT 语句获取的锁,并移除 .cfg 文件。
mysql> USE test;
mysql> UNLOCK TABLES;
  1. 在目标环境,导入表空间。
mysql> USE test;
mysql> ALTER TABLE t1 IMPORT TABLESPACE;
Importing Table Partitions

迁移表分区的步骤:

  1. 在目标环境,创建与原环境相同的分区表,可以使用 SHOW CREATE TABLE 语句获取表定义。
mysql> USE test;
mysql> CREATE TABLE t1 (i int) ENGINE = InnoDB PARTITION BY KEY (i) PARTITIONS 4;

在对应的模式目录下,可以看到每个分区对应的 .ibd 文件。

mysql> \! ls /path/to/datadir/test/
t1#p#p0.ibd  t1#p#p1.ibd  t1#p#p2.ibd t1#p#p3.ibd
  1. 在目标环境,丢弃需要迁移的表分区,将会移除对应的 .ibd 文件。
mysql> ALTER TABLE t1 DISCARD PARTITION p2, p3 TABLESPACE;
mysql> \! ls /path/to/datadir/test/
t1#p#p0.ibd  t1#p#p1.ibd
  1. 在源环境,使用 FLUSH TABLES ... FOR EXPORT 语句静默要迁移的分区表,只允许在该表上进行只读事务。该语句运行时,InnoDB 在表的模式目录中为每个表空间文件生成 .cfg 元数据文件,该文件包含在导入操作期间用于模式验证的元数据。
mysql> USE test;
mysql> FLUSH TABLES t1 FOR EXPORT;

mysql> \! ls /path/to/datadir/test/
t1#p#p0.ibd  t1#p#p1.ibd  t1#p#p2.ibd t1#p#p3.ibd
t1#p#p0.cfg  t1#p#p1.cfg  t1#p#p2.cfg t1#p#p3.cfg
  1. 在源环境,将 .ibd 文件和 .cfg 元数据文件复制到目标环境,权限需要保持一致。如果目标环境是主从架构,则需要将文件复制到主库和从库。
$> scp t1#p#p2.ibd t1#p#p2.cfg t1#p#p3.ibd t1#p#p3.cfg destination-server:/path/to/datadir/test

注意:如果是加密表空间,还需要拷贝 .cfp 文件。

  1. 在源环境,使用 UNLOCK TABLES 释放 FLUSH TABLES ... FOR EXPORT 语句获取的锁,并移除 .cfg 文件。
mysql> USE test;
mysql> UNLOCK TABLES;
  1. 在目标环境,导入表分区。
mysql> USE test;
mysql> ALTER TABLE t1 IMPORT PARTITION p2, p3 TABLESPACE;
Limitations
  • 传输表空间只支持独立表空间(file-per-table tablespaces),不支持位于系统表空间和常规表空间的表。共享表空间的表不能被静默。
  • FLUSH TABLES ... FOR EXPORT 语句不支持有 FULLTEXT 索引的表。可以在导出前先删除,导入后再重建。
Internals

目标环境执行 ALTER TABLE ... DISCARD TABLESPACE 会:

  • The table is locked in X mode.
  • The tablespace is detached from the table.

源环境执行 FLUSH TABLES ... FOR EXPORT 会:

  • The table being flushed for export is locked in shared mode.
  • The purge coordinator thread is stopped.
  • Dirty pages are synchronized to disk.
  • Table metadata is written to the binary .cfg file.

错误日志信息如下:

[Note] InnoDB: Sync to disk of '"test"."t1"' started.
[Note] InnoDB: Stopping purge
[Note] InnoDB: Writing table metadata to './test/t1.cfg'
[Note] InnoDB: Table '"test"."t1"' flushed to disk

源环境执行 UNLOCK TABLES 会:

  • The binary .cfg file is deleted.
  • The shared lock on the table or tables being imported is released and the purge coordinator thread is restarted.

错误日志信息如下:

[Note] InnoDB: Deleting the meta-data file './test/t1.cfg'
[Note] InnoDB: Resuming purge

目标环境执行 ALTER TABLE ... IMPORT TABLESPACE 会:

  • Each tablespace page is checked for corruption.
  • The space ID and log sequence numbers (LSNs) on each page are updated.
  • Flags are validated and LSN updated for the header page.
  • Btree pages are updated.
  • The page state is set to dirty so that it is written to disk.

错误日志信息如下:

[Note] InnoDB: Importing tablespace for table 'test/t1' that was exported
from host 'host_name'
[Note] InnoDB: Phase I - Update all pages
[Note] InnoDB: Sync to disk
[Note] InnoDB: Sync to disk - done!
[Note] InnoDB: Phase III - Flush changes to disk
[Note] InnoDB: Phase IV - Flush complete

Moving or Copying InnoDB Tables

为避免表名大小写在各个平台不兼容问题,建议在初始化服务器时设置参数:

[mysqld]
lower_case_table_names=1
Copying Data Files

移动 .ibd 文件及其表到其他数据库,使用 RENAME TABLE 语句:

RENAME TABLE db1.tbl_name TO db2.tbl_name;

AUTO_INCREMENT Handling in InnoDB

InnoDB 提供了一种可配置的锁机制,在向具有 AUTO_INCREMENT 字段的表插入数据时,可以显著提高 SQL 语句的性能。要在 InnoDB 表中使用 AUTO_INCREMENT 机制,则必须将 AUTO_INCREMENT 字段定义为某个索引的第一列或唯一列,索引最好是 PRIMARY KEY 或者 UNIQUE

InnoDB AUTO_INCREMENT Lock Modes

Insert 分类:

  • “Simple inserts”:可以预先确定要插入的行数的语句。包括没有嵌套子查询的单行或多行 INSERTREPLACE 语句。
  • “Bulk inserts”:事先不知道要插入的行数(以及所需的自增数)的语句。包括 INSERT ... SELECTREPLACE ... SELECTLOAD DATA 语句。
  • “Mixed-mode inserts”:一类是为部分记录指定自增值的 “simple insert” 语句,例如:
INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');

另一类是 INSERT ... ON DUPLICATE KEY UPDATE 语句。

  • “INSERT-like”:包括 “Simple inserts”,“Bulk inserts” 和 “Mixed-mode inserts”。

使用参数 innodb_autoinc_lock_mode 配置自增锁模式,可以设置为 0,1 或者 2,分别表示 “traditional”,“consecutive” 或者 “interleaved” 锁模式。从MySQL 8.0开始,“interleaved” 锁模式(innodb_autoinc_lock_mode=2)是默认值。在MySQL 8.0之前,“consecutive” 锁模式是默认值(innodb_autoinc_lock_mode=1)。

  • “traditional” 锁模式:在这种锁模式下,所有 “INSERT-like” 语句获取特殊的表级 AUTO-INC 锁,用于插入到具有AUTO_INCREMENT列的表中。此锁通常保留到语句结束(而不是事务结束),以确保自增值连续,但影响并发性能。适用于 statement-based 复制。
  • “consecutive” 锁模式:在这种锁模式下,“bulk inserts” 获取特殊的表级 AUTO-INC 锁直到语句结束,“Simple inserts” 通过在互斥锁(轻量级锁)的控制下获取所需数量的自增值来避免表级 AUTO-INC 锁。此锁模式可确保在存在事先不知道行数的 INSERT 语句的情况下(随着语句的进行分配自增值),任何 “INSERT-like” 语句分配的所有自增值都是连续的,并且对于 statement-based 复制操作是安全的。但对于 “mixed-mode inserts”,InnoDB 分配的自增值多于要插入的行数。
  • “interleaved” 锁模式:在这种锁模式下,没有 “INSERT-like” 语句使用表级 AUTO-INC 锁,并且可以同时执行多个语句,自增值不保证连续,这是最快且最具可伸缩性的锁模式。但不适用于 statement-based 复制。
InnoDB AUTO_INCREMENT Lock Mode Usage Implications

如果使用 statement-based 复制,设置参数 innodb_autoinc_lock_mode 为 0 或者 1。如果使用 row-based 或者 mixed-format 复制,所有锁模式都是安全的。

在所有自增锁模式中,如果生成自增值的事务回滚了,则这些自增值就丢失了。丢失的值不会重复使用,因此自增列中的值就会存在间隙。

在所有自增锁模式中,在 INSERT 时为为自增字段指定为 NULL 或者 0,InnoDB 会忽略并产生自增值。

在 MySQL 5.7 及更早版本中,修改一系列 INSERT 语句中间的 AUTO_INCREMENT 列值可能会导致 “Duplicate entry” 错误。在 MySQL 8.0 及更高版本中,如果将 AUTO_INCREMENT 列值修改为大于当前最大自增值的值,则会保留新值,并且后续的 INSERT 操作将从新的较大值开始分配自增值。以下示例演示了此行为:

mysql> CREATE TABLE t1 (
    -> c1 INT NOT NULL AUTO_INCREMENT,
    -> PRIMARY KEY (c1)
    ->  ) ENGINE = InnoDB;

mysql> INSERT INTO t1 VALUES(0), (0), (3);

mysql> SELECT c1 FROM t1;
+----+
| c1 |
+----+
|  1 |
|  2 |
|  3 |
+----+

mysql> UPDATE t1 SET c1 = 4 WHERE c1 = 1;

mysql> SELECT c1 FROM t1;
+----+
| c1 |
+----+
|  2 |
|  3 |
|  4 |
+----+

mysql> INSERT INTO t1 VALUES(0);

mysql> SELECT c1 FROM t1;
+----+
| c1 |
+----+
|  2 |
|  3 |
|  4 |
|  5 |
+----+
InnoDB AUTO_INCREMENT Counter Initialization

如果为 InnoDB 表指定 AUTO_INCREMENT 列,则内存中表对象将包含一个称为自增计数器的特殊计数器,用于为列分配新值。

在 MySQL 5.7 及更早版本中,自增计数器存储在内存中。为了在服务器重启后初始化自增计数器,InnoDB 将在第一次插入到包含 AUTO_INCREMENT 列的表中时执行类似以下语句:

SELECT MAX(ai_col) FROM table_name FOR UPDATE;

与之前版本不同,在 MySQL 8.0 中,当前最大自增计数器值每次更改时都会写入重做日志,并在每个检查点时保存到数据字典中,这样就使当前的最大自增计数器值在服务器重启后保持不变。

初始化自增计数器后,如果在插入行时未显式指定自增值,InnoDB 将隐式递增计数器并将新值分配给列。如果插入显式指定自增列值的行,并且该值大于当前最大计数器值,则该计数器将设置为显示指定的值。

使用语句 ALTER TABLE ... AUTO_INCREMENT = N 修改自增计数器的值,且该值需要比当前最大值大。

使用参数 auto_increment_offset 指定自增列的起始值,默认值为 1,范围为 1 到 65535。

使用参数 auto_increment_increment 指定步长,默认值为 1,范围为 1 到 65535。

Indexes

Clustered and Secondary Indexes

每个 InnoDB 表都有一个称为聚簇索引的特殊索引,用于存储行数据。通常,聚簇索引即是主键。

  • 当在表上定义主键时,InnoDB 会将其用作聚簇索引。应为每个表定义一个主键,如果没有逻辑唯一且非空的单列或多列来作为主键,则使用自增列。
  • 如果没有为表定义主键,InnoDB 将使用第一个所有键列都定义为非空的唯一索引作为聚簇索引。
  • 如果表没有主键和合适的唯一索引,InnoDB 会在包含行 ID 值的合成列上生成一个名为 GEN_CLUST_INDEX 的隐藏聚簇索引。

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

如果主键较长,则二级索引占用更多空间,因此建议使用较短的主键。

The Physical Structure of an InnoDB Index

除空间索引外,InnoDB 索引是 B 树数据结构。

参数 innodb_page_size 用于在初始化 MySQL 实例时指定索引页大小,默认是 16 KB。

当新记录插入 InnoDB 聚簇索引时,InnoDB 会尝试保留页的 1/16,以供将来插入和更新索引记录。如果按顺序(升序或降序)插入索引记录,则索引页大约占满 15/16 。如果以随机顺序插入记录,则占满 1/2 到 15/16。

InnoDB 在创建或重建 B 树索引时执行批量加载。这种索引创建方法称为排序索引构建(sorted index build)。参数 innodb_fill_factor 定义在排序索引构建期间填充的每个 B 树页上的空间百分比,剩余空间保留用于将来的索引增长。如果参数 innodb_fill_factor 设置为 100(默认值),则聚簇索引页中 1/16 的空间可供将来的索引增长使用。

Sorted Index Builds

MySQL 在创建或者重建索引时,采用批量创建索引的方式称为排序索引构建。排序索引构建不支持空间索引。

索引构建分为三个阶段。在第一阶段,通过扫描聚簇索引产生需要构建索引的索引项并放到排序缓冲区中。当排序缓冲区满时,这些索引项会被排序并且写入到一个临时文件中。这步处理也被称为 run。在第二阶段,当一个或多个 runs 被写入临时文件,会对这些临时中的所有索引项进行合并排序。在第三阶段,这些排过序的索引项会被插入到 B-tree 中。

Tablespaces

The System Tablespace

系统表空间是写缓冲区的存储区域。如果表创建在系统表空间,而不是独立表空间或者常规表空间,则系统表空间还包含表和索引数据。在之前的版本中,系统表空间包含 InnoDb 数据字典,在 MySQL 8.0,InnoDB 存储元数据到 MySQL 数据字典。在之前的版本中,系统表空间还包含双写缓冲区存储区域,从 MySQL 8.0.20 开始,此存储区域位于单独双写文件中。

系统表空间可以有一个或多个数据文件。缺省情况下,将在数据目录中创建一个名为 ibdata1 的单个系统表空间数据文件。系统表空间数据文件的大小和数量由参数 innodb_data_file_path 指定。

[(none)]> SHOW VARIABLES LIKE 'innodb_data_file_path';
+-----------------------+------------------------+
| Variable_name         | Value                  |
+-----------------------+------------------------+
| innodb_data_file_path | ibdata1:12M:autoextend |
+-----------------------+------------------------+
1 row in set (0.01 sec)

默认值为 ibdata1:12M:autoextend,表示创建一个名为 ibdata1 的单个自动扩展数据文件,略大于 12MB。

参数 innodb_autoextend_increment 指定系统表空间自动增长值,默认为 64M。该参数不影响独立表空间和常规表空间,其自动增长值为 4MB。

Resizing the System Tablespace

通过添加另一个数据文件来增加系统表空间大小的步骤:

  1. 停止 MySQL Server。
  2. 如果参数 innodb_data_file_path 中最后一个数据文件定义了自动扩展属性,则移除该属性,并根据文件当前大小修改大小属性。
  3. 向参数 innodb_data_file_path 中增加一个数据文件,可以加上自动扩展属性,该属性只能加在最后一个数据文件上。
  4. 启动 MySQL Server。

例子:为系统表空间增加数据文件

修改前

innodb_data_home_dir =
innodb_data_file_path = /ibdata/ibdata1:10M:autoextend

修改后

innodb_data_home_dir =
innodb_data_file_path = /ibdata/ibdata1:988M;/disk2/ibdata2:50M:autoextend

File-Per-Table Tablespaces

独立表空间包含单个 InnoDB 表的数据和索引,并存储在文件系统的单个数据文件中,最大 64TB。独立表空间可以在截断和删除表后回收磁盘空间到操作系统,提高 TRUNCATE TABLE 性能,更易于备份、恢复与监控,建议使用独立表空间。

File-Per-Table Tablespace Configuration

InnoDB 默认在独立表空间创建表,由参数 innodb_file_per_table 指定。禁用该参数将在系统表空间创建表。

可以在参数文件中配置该参数:

[mysqld]
innodb_file_per_table=ON

也可以在运行时使用 SET GLOBAL 语句:

mysql> SET GLOBAL innodb_file_per_table=ON;
File-Per-Table Tablespace Data Files

独立表空间创建在 MySQL 数据目录下的某个模式目录中的 .idb 数据文件中,.idb 以表名命名。

例子:数据目录为 /path/to/mysql/data/,模式为 test,表名为 t1

mysql> USE test;

mysql> CREATE TABLE t1 (
   id INT PRIMARY KEY AUTO_INCREMENT,
   name VARCHAR(100)
 ) ENGINE = InnoDB;

$> cd /path/to/mysql/data/test
$> ls
t1.ibd

Undo Tablespaces

UNDO 表空间包含 UNDO 日志。

Default Undo Tablespaces

初始化 MySQL 实例时会创建两个默认 UNDO 表空间。至少需要两个 UNDO 表空间才能支持 UNDO 表空间的自动截断。

默认 UNDO 表空间创建在参数 innodb_undo_directory 指定的位置。

[(none)]> SHOW VARIABLES LIKE 'innodb_undo_directory';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_undo_directory | ./    |
+-----------------------+-------+
1 row in set (0.01 sec)

如果未定义 innodb_undo_directory 参数,则会在数据目录中创建默认 UNDO 表空间。默认 UNDO 表空间数据文件命名为 undo_001undo_002

[root@mysql ~]# ll /data/mysql/undo_00*
-rw-r----- 1 mysql mysql 16777216 Mar 14 13:13 /data/mysql/undo_001
-rw-r----- 1 mysql mysql 16777216 Mar 14 13:13 /data/mysql/undo_002

数据字典中对应的 UNDO 表空间名称为 innodb_undo_001innodb_undo_002

[(none)]> select name,path from information_schema.INNODB_TABLESPACES_BRIEF where name like '%undo%';
+-----------------+------------+
| NAME            | PATH       |
+-----------------+------------+
| innodb_undo_001 | ./undo_001 |
| innodb_undo_002 | ./undo_002 |
+-----------------+------------+
2 rows in set (0.00 sec)

[(none)]> select * from information_schema.INNODB_TABLESPACES where SPACE_TYPE='Undo'\G
*************************** 1. row ***************************
          SPACE: 4294967279
           NAME: innodb_undo_001
           FLAG: 0
     ROW_FORMAT: Undo
      PAGE_SIZE: 16384
  ZIP_PAGE_SIZE: 0
     SPACE_TYPE: Undo
  FS_BLOCK_SIZE: 4096
      FILE_SIZE: 16777216
 ALLOCATED_SIZE: 16777216
AUTOEXTEND_SIZE: 0
 SERVER_VERSION: 8.0.32
  SPACE_VERSION: 1
     ENCRYPTION: N
          STATE: active
*************************** 2. row ***************************
          SPACE: 4294967278
           NAME: innodb_undo_002
           FLAG: 0
     ROW_FORMAT: Undo
      PAGE_SIZE: 16384
  ZIP_PAGE_SIZE: 0
     SPACE_TYPE: Undo
  FS_BLOCK_SIZE: 4096
      FILE_SIZE: 16777216
 ALLOCATED_SIZE: 16777216
AUTOEXTEND_SIZE: 0
 SERVER_VERSION: 8.0.32
  SPACE_VERSION: 1
     ENCRYPTION: N
          STATE: active
2 rows in set (0.00 sec)
Undo Tablespace Size

在 MySQL 8.0.23 之前,UNDO 表空间的初始大小取决于 innodb_page_size。对于默认的 16KB 页大小,初始 UNDO 表空间文件大小为 10MB。对于 4KB、8KB、32KB 和 64KB 页大小,初始 UNDO 表空间文件大小分别为 7MB、8MB、20MB 和 40MB。从 MySQL 8.0.23 开始,初始 UNDO 表空间大小通常为 16MB。

在 MySQL 8.0.23 之前,UNDO 表空间一次扩展四个区。从 MySQL 8.0.23 开始,UNDO 表空间至少扩展16MB。如果上一次扩展发生在 0.1 秒之内,则文件扩展大小将加倍。扩展大小可能会多次加倍,最大为 256MB。如果上一次扩展发生在 0.1 秒之外,则扩展大小将减少一半(也可以多次出现),最少为 16MB。如果为 UNDO 表空间定义了 AUTOEXTEND_SIZE 选项,那么将按 AUTOEXTEND_SIZE 设置和前面描述的扩展大小中的较大者进行扩展。

Adding Undo Tablespaces

在长时间运行的事务期间 UNDO 日志可能会变得很大,创建额外的 UNDO 表空间可以防止单个 UNDO 表空间变得太大,避免影响性能。从 MySQL 8.0.14 开始,可以在运行时使用 CREATE UNDO TABLESPACE 语法创建 UNDO 表空间。

CREATE UNDO TABLESPACE tablespace_name ADD DATAFILE 'file_name.ibu';

UNDO 表空间文件名必须具有 .ibu 扩展名,且不允许指定相对路径。路径来自于参数 innodb_directories,默认为 NULL,但是在 MySQL 启动时,会自动将参数 innodb_data_home_dirinnodb_undo_directorydatadir 中的目录添加到参数 innodb_directories

[(none)]> SHOW VARIABLES LIKE 'innodb_directories';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| innodb_directories |       |
+--------------------+-------+
1 row in set (0.01 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_data_home_dir';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| innodb_data_home_dir |       |
+----------------------+-------+
1 row in set (0.00 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_undo_directory';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_undo_directory | ./    |
+-----------------------+-------+
1 row in set (0.01 sec)

[(none)]> SHOW VARIABLES LIKE 'datadir';
+---------------+--------------+
| Variable_name | Value        |
+---------------+--------------+
| datadir       | /data/mysql/ |
+---------------+--------------+
1 row in set (0.01 sec)

如果 UNDO 表空间文件名不包含路径,那么将在参数 innodb_undo_directory 定义的目录中创建 UNDO 表空间。如果未定义该参数,则会在数据目录中创建 UNDO 表空间。

要在相对于数据目录的路径中创建 UNDO 表空间,将参数 innodb_undo_directory设置为相对路径,并在创建 UNDO 表空间时仅指定文件名。

查询表 INFORMATION_SCHEMA.FILES 查看 UNDO 表空间名称和路径。

[(none)]> SELECT TABLESPACE_NAME, FILE_NAME FROM INFORMATION_SCHEMA.FILES
    ->    WHERE FILE_TYPE LIKE 'UNDO LOG';
+-----------------+------------+
| TABLESPACE_NAME | FILE_NAME  |
+-----------------+------------+
| innodb_undo_001 | ./undo_001 |
| innodb_undo_002 | ./undo_002 |
+-----------------+------------+
2 rows in set (0.00 sec)

一个 MySQL 实例最多支持 127 个 UNDO 表空间,包括初始化 MySQL 实例时创建的两个默认 UNDO 表空间。

Dropping Undo Tablespaces

从 MySQL 8.0.14 开始,使用 CREATE UNDO TABLESPACE 创建的 UNDO 表空间可以在运行时使用 DROP UNDO TABALESPACE 删除。

只能删除空(empty)的 UNDO 表空间。要清空 UNDO 表空间,必须首先使用 ALTER UNDO TABLESPACE 将 UNDO 表空间标记为非活动(inactive)状态,以便该表空间不再将回滚段分配给新事务。

ALTER UNDO TABLESPACE tablespace_name SET INACTIVE;

在 UNDO 表空间标记为非活动后,其回滚段对应的事务完成后,会释放回滚段,UNDO 表空间将被截断为其初始大小。一旦 UNDO 表空间为空,就可以将其删除。

DROP UNDO TABLESPACE tablespace_name;

查询表 INFORMATION_SCHEMA.INNODB_TABLESPACES 查看 UNDO 表空间状态。

[(none)]> SELECT NAME, STATE FROM INFORMATION_SCHEMA.INNODB_TABLESPACES
    ->    WHERE NAME LIKE 'innodb_undo%';
+-----------------+--------+
| NAME            | STATE  |
+-----------------+--------+
| innodb_undo_001 | active |
| innodb_undo_002 | active |
+-----------------+--------+
2 rows in set (0.00 sec)

非活动(inactive)状态表示 UNDO 空间中的回滚段不再被新事务使用。空(empty)状态表示 UNDO 表空间为空,可被删除,或者可以使用 ALTER UNDO TABLESPACE tablespace_name SET ACTIVE 语句再次激活。尝试删除不为空的 UNDO 表空间将返回错误。

无法删除初始化 MySQL 实例时创建的默认 UNDO 表空间(innodb_undo_001 和 innodb_undo_002)。但是,可以使用 ALTER UNDO TABLESPACE tablespace_name SET INACTIVE 语句使其处于非活动状态。在默认 UNDO 表空间变为非活动状态之前,必须有一个 UNDO 表空间来取代它。始终至少需要两个活动的 UNDO表空间,以支持 UNDO 表空间的自动截断。

Truncating Undo Tablespaces

可以使用配置参数自动截断 UNDO 表空间,也可以使用 SQL 语句手动截断 UNDO 表空间。

自动截断至少需要两个活动的 UNDO 表空间。设置参数 innodb_undo_log_truncateON 启用自动截断 UNDO 表空间。

mysql> SET GLOBAL innodb_undo_log_truncate=ON;

启用参数 innodb_undo_log_truncate 后,超过参数 innodb_max_undo_log_size (默认值为 1024MB)限制的 UNDO 表空间将被截断到其初始大小。

[(none)]> SELECT @@innodb_max_undo_log_size;
+----------------------------+
| @@innodb_max_undo_log_size |
+----------------------------+
|                 4294967296 |
+----------------------------+
1 row in set (0.00 sec)

为加快自动截断,可以调小参数 innodb_purge_rseg_truncate_frequency,默认为 128:

mysql> SELECT @@innodb_purge_rseg_truncate_frequency;
+----------------------------------------+
| @@innodb_purge_rseg_truncate_frequency |
+----------------------------------------+
|                                    128 |
+----------------------------------------+

mysql> SET GLOBAL innodb_purge_rseg_truncate_frequency=32;

手动截断 UNDO 表空间至少需要三个活动的 UNDO 表空间,先将要截断的 UNDO 表空间设置为不活动状态:

ALTER UNDO TABLESPACE tablespace_name SET INACTIVE;

再查询其状态:

SELECT NAME, STATE FROM INFORMATION_SCHEMA.INNODB_TABLESPACES
  WHERE NAME LIKE 'tablespace_name';

待该 UNDO 表空间状态变为 empty 时,设置为活动状态:

ALTER UNDO TABLESPACE tablespace_name SET ACTIVE;

查询表 INFORMATION_SCHEMA.INNODB_METRICS 获取截断相关计数器信息。

[(none)]> SELECT NAME, SUBSYSTEM, COMMENT FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME LIKE '%truncate%';
+-----------------------------------+-----------+------------------------------------------------------------------------+
| NAME                              | SUBSYSTEM | COMMENT                                                                |
+-----------------------------------+-----------+------------------------------------------------------------------------+
| purge_truncate_history_count      | purge     | Number of times the purge thread attempted to truncate undo history    |
| purge_truncate_history_usec       | purge     | Time (in microseconds) the purge thread spent truncating undo history. |
| undo_truncate_count               | undo      | Number of times undo truncation was initiated                          |
| undo_truncate_start_logging_count | undo      | Number of times during undo truncation a log file was started          |
| undo_truncate_done_logging_count  | undo      | Number of times during undo truncation a log file was deleted          |
| undo_truncate_usec                | undo      | Time (in microseconds) spent to process undo truncation                |
+-----------------------------------+-----------+------------------------------------------------------------------------+
6 rows in set (0.00 sec)

UNDO 表空间截断操作会在参数 innodb_log_group_home_dir 目录中创建一个名称类似于 undo_space_number_trunc.log 的临时文件。如果在截断操作期间发生系统故障,可以在系统启动时使用临时日志文件继续截断操作。

Undo Tablespace Status Variables

查看 UNDO 表空间总数、隐式(InnoDB 创建的)UNDO 表空间、显式(用户创建的)UNDO 表空间以及活动 UNDO 表空间的数量:

[(none)]> SHOW STATUS LIKE 'Innodb_undo_tablespaces%';
+----------------------------------+-------+
| Variable_name                    | Value |
+----------------------------------+-------+
| Innodb_undo_tablespaces_total    | 2     |
| Innodb_undo_tablespaces_implicit | 2     |
| Innodb_undo_tablespaces_explicit | 0     |
| Innodb_undo_tablespaces_active   | 2     |
+----------------------------------+-------+
4 rows in set (0.00 sec)

Temporary Tablespaces

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

Session Temporary Tablespaces

会话临时表空间存储用户创建的临时表和优化器创建的内部临时表。

会话临时表空间在第一次请求创建磁盘临时表时从临时表空间池分配给会话。最多为会话分配两个表空间,一个用于用户创建的临时表,另一个用于优化器创建的内部临时表。

当会话断开连接时,其临时表空间将被截断并释放回池。启动服务器时,将创建一个包含 10 个临时表空间的池。池不会缩小,表空间会根据需要自动添加到池中。临时表空间池在正常关闭时被删除。会话临时表空间文件在创建时的大小为 5 页,扩展名为 .ibt

参数 innodb_temp_tablespaces_dir 指定会话临时表空间位置,默认位于数据目录的 #innodb_temp 文件夹下。

[(none)]> SHOW VARIABLES LIKE 'innodb_temp_tablespaces_dir';
+-----------------------------+-----------------+
| Variable_name               | Value           |
+-----------------------------+-----------------+
| innodb_temp_tablespaces_dir | ./#innodb_temp/ |
+-----------------------------+-----------------+
1 row in set (0.00 sec)

[(none)]> system ls -l /data/mysql/#innodb_temp/
total 800
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_10.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_1.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_2.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_3.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_4.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_5.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_6.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_7.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_8.ibt
-rw-r----- 1 mysql mysql 81920 Mar 14 13:11 temp_9.ibt

查询表 information_schema.INNODB_SESSION_TEMP_TABLESPACES 获取会话临时表空间信息。

[(none)]> select * from information_schema.INNODB_SESSION_TEMP_TABLESPACES;
+----+------------+----------------------------+-------+----------+-----------+
| ID | SPACE      | PATH                       | SIZE  | STATE    | PURPOSE   |
+----+------------+----------------------------+-------+----------+-----------+
|  9 | 4243767290 | ./#innodb_temp/temp_10.ibt | 81920 | ACTIVE   | INTRINSIC |
|  0 | 4243767281 | ./#innodb_temp/temp_1.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4243767282 | ./#innodb_temp/temp_2.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4243767283 | ./#innodb_temp/temp_3.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4243767284 | ./#innodb_temp/temp_4.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4243767285 | ./#innodb_temp/temp_5.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4243767286 | ./#innodb_temp/temp_6.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4243767287 | ./#innodb_temp/temp_7.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4243767288 | ./#innodb_temp/temp_8.ibt  | 81920 | INACTIVE | NONE      |
|  0 | 4243767289 | ./#innodb_temp/temp_9.ibt  | 81920 | INACTIVE | NONE      |
+----+------------+----------------------------+-------+----------+-----------+
10 rows in set (0.00 sec)

查询表 information_schema.INNODB_TEMP_TABLE_INFO 获取用户创建的临时表信息。

[(none)]> select * from information_schema.INNODB_TEMP_TABLE_INFO;
Empty set (0.00 sec)
Global Temporary Tablespace

全局临时表空间(ibtmp1)存储对用户创建的临时表所做更改的回滚段。

参数 innodb_temp_data_file_path 指定全局临时表空间数据文件的相对路径、名称、大小和属性。如果未指定任何值,则默认是在参数 innodb_data_home_dir 指定的目录中创建一个名为 ibtmp1 的自动扩展数据文件,初始大小略大于 12MB。

[(none)]> SHOW VARIABLES LIKE 'innodb_temp_data_file_path';
+----------------------------+-----------------------+
| Variable_name              | Value                 |
+----------------------------+-----------------------+
| innodb_temp_data_file_path | ibtmp1:12M:autoextend |
+----------------------------+-----------------------+
1 row in set (0.00 sec)

使用 max 选项限制全局临时表空间数据文件的大小,需要重启才能生效。例如:

[mysqld]
innodb_temp_data_file_path=ibtmp1:12M:autoextend:max:10G

全局临时表空间在正常关闭或中止初始化时被删除,并在每次启动服务器时重新创建。如果无法创建全局临时表空间,则拒绝启动。如果服务器意外停止,则不会删除全局临时表空间。在这种情况下,数据库管理员可以手动删除全局临时表空间或重新启动 MySQL 服务器。重新启动 MySQL 服务器会自动删除并重新创建全局临时表空间。

查询表 INFORMATION_SCHEMA.FILES 获取全局临时表空间信息。

[(none)]> SELECT * FROM INFORMATION_SCHEMA.FILES WHERE TABLESPACE_NAME='innodb_temporary'\G
*************************** 1. row ***************************
             FILE_ID: 4294967293
           FILE_NAME: ./ibtmp1
           FILE_TYPE: TEMPORARY
     TABLESPACE_NAME: innodb_temporary
       TABLE_CATALOG: 
        TABLE_SCHEMA: NULL
          TABLE_NAME: NULL
  LOGFILE_GROUP_NAME: NULL
LOGFILE_GROUP_NUMBER: NULL
              ENGINE: InnoDB
       FULLTEXT_KEYS: NULL
        DELETED_ROWS: NULL
        UPDATE_COUNT: NULL
        FREE_EXTENTS: 2
       TOTAL_EXTENTS: 12
         EXTENT_SIZE: 1048576
        INITIAL_SIZE: 12582912
        MAXIMUM_SIZE: NULL
     AUTOEXTEND_SIZE: 67108864
       CREATION_TIME: NULL
    LAST_UPDATE_TIME: NULL
    LAST_ACCESS_TIME: NULL
        RECOVER_TIME: NULL
 TRANSACTION_COUNTER: NULL
             VERSION: NULL
          ROW_FORMAT: NULL
          TABLE_ROWS: NULL
      AVG_ROW_LENGTH: NULL
         DATA_LENGTH: NULL
     MAX_DATA_LENGTH: NULL
        INDEX_LENGTH: NULL
           DATA_FREE: 6291456
         CREATE_TIME: NULL
         UPDATE_TIME: NULL
          CHECK_TIME: NULL
            CHECKSUM: NULL
              STATUS: NORMAL
               EXTRA: NULL
1 row in set (0.00 sec)

[(none)]> SELECT FILE_NAME, TABLESPACE_NAME, ENGINE, INITIAL_SIZE, TOTAL_EXTENTS*EXTENT_SIZE
    ->    AS TotalSizeBytes, DATA_FREE, MAXIMUM_SIZE FROM INFORMATION_SCHEMA.FILES
    ->    WHERE TABLESPACE_NAME = 'innodb_temporary'\G
*************************** 1. row ***************************
      FILE_NAME: ./ibtmp1
TABLESPACE_NAME: innodb_temporary
         ENGINE: InnoDB
   INITIAL_SIZE: 12582912
 TotalSizeBytes: 12582912
      DATA_FREE: 6291456
   MAXIMUM_SIZE: NULL
1 row in set (0.00 sec)

Moving Tablespace Files While the Server is Offline

参数 innodb_directories 指定在启动时要扫描的表空间目录,支持在服务器脱机时将表空间文件移动或还原到新位置。在启动期间,将使用发现的表空间文件并更新数据字典。

在 MySQL 启动时,会自动将参数 innodb_data_home_dirinnodb_undo_directorydatadir 中的目录添加到参数 innodb_directories

移动表空间文件或目录的步骤:

  1. 停止 MySQL Server。
  2. 移动表空间文件或者目录。
  3. 调整对应参数,如果移动了独立表空间文件,则其模式目录需要保持不变。
  4. 启动 MySQL Server。

Disabling Tablespace Path Validation

启动时,InnoDB 会扫描由参数 innodb_directories 定义的目录以查找表空间文件。根据数据字典中记录的路径验证发现的表空间文件的路径。如果路径不匹配,则更新数据字典中的路径。

MySQL 8.0.21 中引入的参数 innodb_validate_tablespace_paths 允许禁用表空间路径验证。此功能适用于不会移动表空间文件的环境。禁用路径验证可缩短具有大量表空间文件的系统的启动时间。如果参数 log_error_verbosity 设置为 3,那么在禁用表空间路径验证时,在启动时打印以下消息:

[InnoDB] Skipping InnoDB tablespace path validation. 
Manually moved tablespace files will not be detected!

Optimizing Tablespace Space Allocation on Linux

从 MySQL 8.0.22 开始,可以优化 InnoDB 在 Linux 上为独立表空间和常规表空间分配空间的方式。默认情况下,当需要额外空间时,InnoDB 会将页分配给表空间,并将 NULL 写入这些页。如果频繁分配新页,则此行为可能会影响性能。从 MySQL 8.0.22 开始,可以在 Linux 系统上禁用参数 innodb_extend_and_initialize,以避免将 NULL 写入新分配的表空间页。同时需要使用 AUTOEXTEND_SIZE 选项增加表空间扩展大小。

Tablespace AUTOEXTEND_SIZE Configuration

独立表空间和常规表空间默认扩展规则如下:

  • 如果表空间小于 1 个区,则一次扩展 1 页。
  • 如果表空间大于 1 个区,小于 32 个区,则一次扩展 1 个区。
  • 如果表空间大于 32 个区,则一次扩展 4 个区。

从 MySQL 8.0.23 开始,可以通过指定 AUTOEXTEND_SIZE 选项来配置独立表空间或常规表空间的扩展量。

独立表空间,使用 CREATE TABLE 或者 ALTER TABLE 语句进行调整:

CREATE TABLE t1 (c1 INT) AUTOEXTEND_SIZE = 4M;
ALTER TABLE t1 AUTOEXTEND_SIZE = 8M;

常规表空间,使用 CREATE TABLESPACE 或者 ALTER TABLESPACE 语句进行调整:

CREATE TABLESPACE ts1 AUTOEXTEND_SIZE = 4M;
ALTER TABLESPACE ts1 AUTOEXTEND_SIZE = 8M;

AUTOEXTEND_SIZE 必须为 4M 的整数倍,否则会报错。

AUTOEXTEND_SIZE 默认值为 0,按照前面描述的默认扩展规则进行扩展。

在 MySQL 8.0.23 中,最大 AUTOEXTEND_SIZE 为 64M。从 MySQL 8.0.24 开始,最大为 4G。

最小 AUTOEXTEND_SIZE 取决于 InnoDB 页大小,如下表所示:

InnoDB Page SizeMinimum AUTOEXTEND_SIZE
4K4M
8K4M
16K4M
32K8M
64K16M

默认 InnoDB 页大小为 16K。

[(none)]> SELECT @@GLOBAL.innodb_page_size;
+---------------------------+
| @@GLOBAL.innodb_page_size |
+---------------------------+
|                     16384 |
+---------------------------+
1 row in set (0.02 sec)

查询 INFORMATION_SCHEMA.INNODB_TABLESPACES 获取表空间的 AUTOEXTEND_SIZE:

[(none)]> SELECT NAME, AUTOEXTEND_SIZE FROM INFORMATION_SCHEMA.INNODB_TABLESPACES WHERE NAME LIKE 'menagerie/pet';
+---------------+-----------------+
| NAME          | AUTOEXTEND_SIZE |
+---------------+-----------------+
| menagerie/pet |               0 |
+---------------+-----------------+
1 row in set (0.00 sec)

Doublewrite Buffer

双写缓冲区是一个存储区域,InnoDB 在其中写入从缓冲池刷新的页,然后将页写入 InnoDB 数据文件中。如果在页写入过程中出现操作系统、存储系统或意外的 mysqld 进程退出异常,InnoDB 可以在崩溃恢复期间从双写缓冲区中找到页的正确副本。

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

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

[root@mysql ~]# ll /data/mysql/*.dblwr
-rw-r----- 1 mysql mysql  589824 Mar 15 13:47 /data/mysql/#ib_16384_0.dblwr
-rw-r----- 1 mysql mysql 8978432 Feb  7 13:39 /data/mysql/#ib_16384_1.dblwr

双写缓冲区的配置参数有:

参数 innodb_doublewrite 控制是否启用双写缓冲区,默认为启用,不支持动态启用和关闭。

[(none)]> SHOW VARIABLES LIKE 'innodb_doublewrite';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| innodb_doublewrite | ON    |
+--------------------+-------+
1 row in set (0.10 sec)

参数 innodb_doublewrite_dir (从 MySQL 8.0.20 引入)指定双写文件的目录,如果不指定,则使用参数 innodb_data_home_dir 指定的目录,默认为数据目录。

[(none)]> SHOW VARIABLES LIKE 'innodb_doublewrite_dir';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_doublewrite_dir |       |
+------------------------+-------+
1 row in set (0.01 sec)

参数 innodb_doublewrite_files 指定双写文件数量,默认为 2,分别为刷新列表双写文件和 LRU 列表双写文件。

[(none)]> SHOW VARIABLES LIKE 'innodb_doublewrite_files';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_doublewrite_files | 2     |
+--------------------------+-------+
1 row in set (0.01 sec)

参数 innodb_doublewrite_pages(在 MySQL 8.0.20 引入)控制每个线程的最大双写页数。如果未指定任何值,则 innodb_doublewrite_pages 设置为 innodb_write_io_threads 值。此参数用于高级性能优化,默认值适合大多数用户。

[(none)]> SHOW VARIABLES LIKE 'innodb_doublewrite_pages';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_doublewrite_pages | 4     |
+--------------------------+-------+
1 row in set (0.00 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_write_io_threads';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_write_io_threads | 4     |
+-------------------------+-------+
1 row in set (0.00 sec)

参数 innodb_doublewrite_batch_size (在 MySQL 8.0.20 引入)控制要批量写入的双写页数。此参数用于高级性能优化,默认值适合大多数用户。

[(none)]> SHOW VARIABLES LIKE 'innodb_doublewrite_batch_size';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_doublewrite_batch_size | 0     |
+-------------------------------+-------+
1 row in set (0.00 sec)

Redo Log

重做日志是一种基于磁盘的数据结构,记录对数据页的更改,用于在崩溃恢复期间更正不完整事务写入的数据。

Configuring Redo Log Capacity (MySQL 8.0.30 or Higher)

从 MySQL 8.0.30 开始,参数 innodb_redo_log_capacity 控制重做日志文件占用的磁盘空间。可以在启动时使用参数文件或运行时使用 SET GLOBAL 语句设置此参数。例如,以下方式都可以将重做日志容量设置为 8GB:

[mysqld]
#innodb_log_file_size=2G
#innodb_log_files_in_group = 3
innodb_redo_log_capacity=8G
SET GLOBAL innodb_redo_log_capacity = 8589934592;
[(none)]> SHOW VARIABLES LIKE 'innodb_redo_log_capacity';
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| innodb_redo_log_capacity | 8589934592 |
+--------------------------+------------+
1 row in set (0.00 sec)

参数 innodb_redo_log_capacity 取代已弃用的参数 innodb_log_files_in_groupinnodb_log_file_size。当设置了 innodb_redo_log_capacity 时,将忽略 innodb_log_files_in_groupinnodb_log_file_size 设置;否则,将使用这两个参数。如果未设置这三个参数,则使用参数 innodb_redo_log_capacity 默认值,即 104857600 字节 (100MB)。最大重做日志容量为 128GB。

重做日志文件位于数据目录的 #innodb_redo 文件夹下,除非参数 innodb_log_group_home_dir 指定了其他目录。

有两种类型的重做日志文件:普通日志文件和备用日志文件。普通重做日志文件是正在使用的日志文件。备用重做日志文件是等待使用的日志文件。InnoDB 总共维护 32 个重做日志文件,每个文件的大小等于 1/32 * innodb_redo_log_capacity

重做日志文件使用 #ib_redoN 命名规则,其中 N 是重做日志文件号。备用重做日志文件用 _tmp 后缀表示。

[root@k8sm1 ~]# ll -rth /data/mysql/#innodb_redo/
total 8.0G
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo1_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo2_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo3_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo4_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo5_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo6_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo7_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo8_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo9_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo10_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo11_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo12_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo13_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo14_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo15_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo16_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo17_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo18_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo19_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo20_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo21_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo22_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo23_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo24_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo25_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo26_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo27_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo28_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo29_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo30_tmp
-rw-r----- 1 mysql mysql 256M Mar 15 15:32 #ib_redo31_tmp
-rw-r----- 1 mysql mysql 192M Mar 15 15:34 #ib_redo0

每个普通重做日志文件都与特定范围的 LSN 值相关联,例如,以下查询显示上一示例中列出的活动重做日志文件的 START_LSN 和 END_LSN 值:

[(none)]> SELECT FILE_NAME, START_LSN, END_LSN FROM performance_schema.innodb_redo_log_files;
+--------------------------+-----------+-----------+
| FILE_NAME                | START_LSN | END_LSN   |
+--------------------------+-----------+-----------+
| ./#innodb_redo/#ib_redo0 |      8192 | 201332736 |
+--------------------------+-----------+-----------+
1 row in set (0.01 sec)

监视重做日志和重做日志容量调整操作的状态变量有:


[(none)]> SHOW STATUS LIKE 'Innodb_redo_log%';
+-------------------------------------+------------+
| Variable_name                       | Value      |
+-------------------------------------+------------+
| Innodb_redo_log_read_only           | OFF        |
| Innodb_redo_log_uuid                | 1075899837 |
| Innodb_redo_log_checkpoint_lsn      | 22429762   |
| Innodb_redo_log_current_lsn         | 22429762   |
| Innodb_redo_log_flushed_to_disk_lsn | 22429762   |
| Innodb_redo_log_logical_size        | 512        |
| Innodb_redo_log_physical_size       | 201326592  |
| Innodb_redo_log_capacity_resized    | 8589934592 |
| Innodb_redo_log_resize_status       | OK         |
| Innodb_redo_log_enabled             | ON         |
+-------------------------------------+------------+
10 rows in set (0.00 sec)

查询 PERFORMANCE_SCHEMA.INNODB_REDO_LOG_FILES 获取活动重置日志文件信息:

[(none)]> SELECT FILE_ID, START_LSN, END_LSN, SIZE_IN_BYTES, IS_FULL, CONSUMER_LEVEL 
    ->    FROM PERFORMANCE_SCHEMA.INNODB_REDO_LOG_FILES;
+---------+-----------+-----------+---------------+---------+----------------+
| FILE_ID | START_LSN | END_LSN   | SIZE_IN_BYTES | IS_FULL | CONSUMER_LEVEL |
+---------+-----------+-----------+---------------+---------+----------------+
|       0 |      8192 | 201332736 |     201326592 |       0 |              0 |
+---------+-----------+-----------+---------------+---------+----------------+
1 row in set (0.00 sec)

Configuring Redo Log Capacity (Before MySQL 8.0.30)

在 MySQL 8.0.30 之前,InnoDB 默认在数据目录中创建两个重做日志文件,分别为 ib_logfile0ib_logfile1,并以循环方式写入这些文件。

修改重做日志文件的数量和大小的步骤:

  1. 关闭 MySQL Server。
  2. 修改参数文件。
[mysqld]
innodb_log_file_size=2G
innodb_log_files_in_group = 3
  1. 启动 MySQL Server。

Redo Log Archiving

MySQL 8.0.17 中引入了重做日志归档功能,将重做日志记录写入到归档文件,解决备份速度跟不上重做日志生成的速度导致重做日志被覆盖问题,用于 MySQL 企业版的备份工具,类似于 Oracle 的联机重做日志文件和归档日志文件。

使用参数 innodb_redo_log_archive_dirs 启用重做日志归档,默认为 NULL,表示不启用。

mysql> SET GLOBAL innodb_redo_log_archive_dirs='label1:directory_path1[;label2:directory_path2;…]';

注意:

  • 目录需要预先存在。
  • 目录权限必须限定,不能任何人都能访问。
  • 目录不能是 datadirinnodb_data_home_dirinnodb_directoriesinnodb_log_group_home_dirinnodb_temp_tablespaces_dirinnodb_tmpdir innodb_undo_directorysecure_file_priv 定义的目录,也不能是这些目录的父目录或子目录。

当支持重做日志归档的备份工具启动备份时,会通过调用 innodb_redo_log_archive_start() 函数来激活重做日志归档。

如果未使用支持重做日志归档的备份工具,也可以手动激活重做日志归档,如下所示:

mysql> SELECT innodb_redo_log_archive_start('label', 'subdir');
+------------------------------------------+
| innodb_redo_log_archive_start('label') |
+------------------------------------------+
| 0                                        |
+------------------------------------------+

或者:

mysql> DO innodb_redo_log_archive_start('label', 'subdir');
Query OK, 0 rows affected (0.09 sec)

注意:

激活重做日志归档(使用 innodb_redo_log_archive_start() )的 MySQL 会话必须在归档期间保持打开状态。需使用同一会话停用重做日志归档(使用 innodb_redo_log_archive_stop() )。如果在显式停用重做日志归档之前终止会话,服务器将隐式停用重做日志归档并删除重做日志归档文件。

其中 label 是由 innodb_redo_log_archive_dirs 定义的标签;subdir 是一个可选参数,用于指定由 label 标识的目录的子目录,必须是简单的目录名称(没有斜杠(/)、反斜杠(\)或冒号(:))。subdir 可以为空、NULL,也可以省略。

拥有 INNODB_REDO_LOG_ARCHIVE 权限的用户才能使用 innodb_redo_log_archive_start()innodb_redo_log_archive_stop() 启用和停用重做日志归档。

重做日志归档文件路径为 directory_identified_by_label/[subdir/]archive.serverUUID.000001.log,例如:

/directory_path/subdirectory/archive.e71a47dc-61f8-11e9-a3cb-080027154b4d.000001.log

备份工具完成复制 InnoDB 数据文件后,会通过调用 innodb_redo_log_archive_stop() 函数来停用重做日志归档。

如果未使用支持重做日志归档的备份工具,也可以手动停用重做日志归档,如下所示:

mysql> SELECT innodb_redo_log_archive_stop();
+--------------------------------+
| innodb_redo_log_archive_stop() |
+--------------------------------+
| 0                              |
+--------------------------------+

或者:

mysql> DO innodb_redo_log_archive_stop();
Query OK, 0 rows affected (0.01 sec)

函数执行完成后,备份工具会从归档文件中查找重做日志数据的相关部分,并将其复制到备份中。

备份工具完成复制重做日志数据并且不再需要重做日志归档文件后,将删除这些归档文件。

Disabling Redo Logging

不要在生产数据库上禁用重做日志。

从 MySQL 8.0.21 开始,可以使用 ALTER INSTANCE DISABLE INNODB REDO_LOG 语句禁用重做日志记录。此功能旨在将数据加载到新的 MySQL 实例中,可避免重做日志写入和双写缓冲,从而加快数据加载速度。

具体步骤如下:

  1. 授予权限。
mysql> GRANT INNODB_REDO_LOG_ENABLE ON *.* to 'data_load_admin';
  1. 禁用重做日志。
mysql> ALTER INSTANCE DISABLE INNODB REDO_LOG;
  1. 查看状态。
mysql> SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_enabled';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Innodb_redo_log_enabled | OFF   |
+-------------------------+-------+
  1. 加载数据。
  2. 启用重做日志。
mysql> ALTER INSTANCE ENABLE INNODB REDO_LOG;
  1. 查看状态。
mysql> SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_enabled';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Innodb_redo_log_enabled | ON    |
+-------------------------+-------+

Undo Logs

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

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

每个 UNDO 表空间和全局临时表空间分别支持最多 128 个回滚段。参数 innodb_rollback_segments 定义回滚段的数量。

回滚段支持的事务数取决于回滚段中的 UNDO 槽数以及每个事务所需的 UNDO 日志数。回滚段中的 UNDO 槽数因 InnoDB 页大小而异。

InnoDB Page SizeNumber of Undo Slots in a Rollback Segment (InnoDB Page Size / 16)
4096 (4KB)256
8192 (8KB)512
16384 (16KB)1024
32768 (32KB)2048
65536 (64KB)4096

一个事务最多分配四个 UNDO 日志,分别对应以下操作类型:

  • 用户定义表的 INSERT 操作。
  • 用户定义表的 UPDATE 和 DELETE 操作。
  • 用户定义临时表的 INSERT 操作。
  • 用户定义临时表的 UPDATE 和 DELETE 操作。

根据事务包含的操作类型,按需分配 UNDO 日志。例如,对常规表和临时表执行 INSERT、UPDATE 和 DELETE 操作的事务需要完全分配四个 UNDO 日志。仅对常规表执行 INSERT 操作的事务只需要一个 UNDO 日志。

对常规表执行操作的事务将从 UNDO 表空间回滚段中分配 UNDO 日志。对临时表执行操作的事务将从全局临时表空间回滚段中分配 UNDO 日志。

鉴于上述因素,以下公式可用于估计 InnoDB 能够支持的并发读写事务的数量。

如果事务执行 INSERT 或者 UPDATE 或者 DELETE 操作,则 InnoDB 能够支持的并发读写事务数为:

(innodb_page_size / 16) * innodb_rollback_segments * number of undo tablespaces

如果事务执行 INSERT 和 UPDATE 或者 DELETE 操作,则 InnoDB 能够支持的并发读写事务数为:

(innodb_page_size / 16 / 2) * innodb_rollback_segments * number of undo tablespaces

如果事务对临时表执行 INSERT 操作,则 InnoDB 能够支持的并发读写事务数为:

(innodb_page_size / 16) * innodb_rollback_segments

如果事务对临时表执行 INSERT 和 UPDATE 或者 DELETE 操作,则 InnoDB 能够支持的并发读写事务数为:

(innodb_page_size / 16 / 2) * innodb_rollback_segments

注意:

在达到 InnoDB 能够支持的并发读写事务数之前,当分配给事务的回滚段用完 UNDO 槽时,可能会遇到并发事务限制错误。在这种情况下,请尝试重新运行事务。

当事务对临时表执行操作时,InnoDB 能够支持的并发读写事务数受分配给全局临时表空间的回滚段数的限制,默认为128。

InnoDB Locking and Transaction Model

InnoDB Locking

Shared and Exclusive Locks

InnoDB 实现标准行级锁,有两种行级锁:

  • shared (S) locks:共享锁,允许持有锁的事务读取行。
  • exclusive (X) locks:排他锁,允许持有锁的事务更新或删除行。

如果事务 T1 在行 r 上持有 S 锁,则其他事务 T2 在行 r 上可以获取 S 锁,不能获取 X 锁。

如果事务 T1 在行 r 上持有 X 锁,则其他事务 T2 在行 r 上不能获取 S 锁,不能获取 X 锁。

注意:普通的查询不会加任何锁。

Intention Locks

InnoDB 为支持多粒度锁(multiple granularity locking),即允许行锁和表锁共存,引入了意向锁。意向锁是表级锁,指示事务稍后对表中的行需要哪种类型的锁(共享或排他)。有两种意向锁:

  • intention shared lock (IS) :意向共享锁,表示事务有意向对表中的行加共享锁。
  • intention exclusive lock (IX):意向排他锁,表示事务有意向对表中的行加独占锁。

例如,SELECT ... FOR SHARE 加 IS 锁,SELECT ... FOR UPDATE 加 IX 锁。

意向锁规则如下:

  • 在事务可以获取表中行的共享锁之前,必须首先获取表上的 IS 锁。

  • 在事务可以获取表中行的排他锁之前,必须首先获取表上的 IX 锁。

兼容性列表:

XIXSIS
XConflictConflictConflictConflict
IXConflictCompatibleConflictCompatible
SConflictConflictCompatibleCompatible
ISConflictCompatibleCompatibleCompatible

Record Locks

记录锁是索引记录上的锁。例如:

SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
UPDATE t SET c2='c2' WHERE c

其中 c1 列必须为主键列或唯一索引列,且查询条件必须为 =

记录锁始终锁定索引记录,对于没有索引的表,InnoDB 会创建一个隐藏的聚簇索引,并使用此索引进行记录锁定。

使用 SHOW ENGINE INNODB STATUS 查看记录锁的事务数据:

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000000274f; asc     'O;;
 2: len 7; hex b60000019d0110; asc        ;;

Gap Locks

间隙锁是对索引记录之间间隙的锁定,或对第一个索引记录之前或最后一个索引记录之后间隙的锁定。

例如,对于以下 SQL:

SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;

由于条件范围 10 到 20 中所有现有值之间的间隙都是被锁定的,故其他事务无法将值 15 插入到 t.c1 列中。

间隙可能存在于单个索引值,多个索引值,甚至为空。

对于使用唯一索引进行等值查询更新时,不需要间隙锁。例如,如果 id 列具有唯一索引,则以下语句仅对 id 值为 100 的行使用索引记录锁:

SELECT * FROM child WHERE id = 100 FOR UPDATE;

如果 id 列没有索引或有非唯一索引,则该语句会锁定前面的间隙。

InnoDB 中的间隙锁的唯一目的是防止其他事务向间隙插入数据。间隙锁可以共存,多个事务可以对同一间隙加锁。

InnoDB 使用间隙锁在事务隔离级别为 REPEATABLE READ 下解决幻读问题,如果修改事务隔离级别为 READ COMMITTED,则会禁用间隙锁,仅用于外键约束检查和重复键检查。

Next-Key Locks

临键锁是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合。

假设索引包含值 10、11、13 和 20,此索引可能的临键锁覆盖以下间隔:

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

使用 SHOW ENGINE INNODB STATUS 查看临键锁的事务数据:

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10080 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000000274f; asc     'O;;
 2: len 7; hex b60000019d0110; asc        ;;

Insert Intention Locks

插入意向锁是一种在行插入之前由 INSERT 操作设置的间隙锁类型。此锁表示插入意向,即插入同一索引间隙的多个事务,如果在间隙中的插入位置不同,则无需相互等待。假设存在值为 4 和 7 的索引记录,两个事务分别插入值 5 和 6,在获得插入行的排他锁之前,每个事务都使用插入意向锁锁定 4 和 7 之间的间隙,但不会相互阻塞。

例如客户端 A 创建一个包含两个索引记录(90 和 102)的表,然后启动一个事务,该事务对 ID 大于 100 的索引记录加排他锁,排他锁包括记录 102 之前的间隙锁。

mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);

mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id  |
+-----+
| 102 |
+-----+

客户端 B 启动一个事务,将记录插入间隙。事务在等待获取排他锁时采用插入意向锁。

mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);

使用 SHOW ENGINE INNODB STATUS 查看插入意向锁的事务数据:

RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`
trx id 8731 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000066; asc    f;;
 1: len 6; hex 000000002215; asc     " ;;
 2: len 7; hex 9000000172011c; asc     r  ;;...

AUTO-INC Locks

AUTO-INC 锁是一种特殊的表级锁,由插入到具有 AUTO_INCREMENT 列的表中的事务所采用。在最简单的情况下,如果一个事务正在向表中插入值,则其他插入事务都必须等待,以便第一个事务插入的行获取连续的主键值。

参数 innodb_autoinc_lock_mode 指定自增锁的算法。

InnoDB Transaction Model

InnoDB 事务模型旨在将多版本数据库的最佳属性与传统的两阶段锁定相结合。与Oracle类似,InnoDB 默认在行级别执行锁定,并以非锁定一致读方式运行查询。

Transaction Isolation Levels

InnoDB 支持 SQL1992 标准中的所有四种事务隔离级别:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

用户可以使用 SET TRANSACTION 语句更改单个会话或所有后续连接的隔离级别,在参数文件中使用 transaction_isolation 设置服务器的隔离级别。

InnoDB 使用不同的锁策略支持各个事务隔离级别。

REPEATABLE READ

这是 InnoDB 默认隔离级别。

对于 SELECT,在同一事务中使用一致读确保相同查询在不同时间的查询结果一致。

对于可重复读,例如,客户端 A 开启事务执行查询:

[menagerie]> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.07 sec)

[menagerie]> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

[menagerie]> SELECT * FROM test;
+----+----------+---------------------+---------------------+
| id | name     | create_time         | update_time         |
+----+----------+---------------------+---------------------+
|  1 | stonebox | 2023-03-06 10:07:55 | 2023-03-06 10:08:53 |
+----+----------+---------------------+---------------------+
1 row in set (0.00 sec)

客户端 B 修改数据:

[menagerie]> UPDATE test SET name='stone' WHERE id=1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

[menagerie]> SELECT * FROM test;
+----+-------+---------------------+---------------------+
| id | name  | create_time         | update_time         |
+----+-------+---------------------+---------------------+
|  1 | stone | 2023-03-06 10:07:55 | 2023-03-24 16:28:15 |
+----+-------+---------------------+---------------------+
1 row in set (0.00 sec)

客户端 A 在事务内再次查询,结果不变:

[menagerie]> SELECT * FROM test;
+----+----------+---------------------+---------------------+
| id | name     | create_time         | update_time         |
+----+----------+---------------------+---------------------+
|  1 | stonebox | 2023-03-06 10:07:55 | 2023-03-06 10:08:53 |
+----+----------+---------------------+---------------------+
1 row in set (0.01 sec)

对于幻读,例如,客户端 A 开启事务执行查询:

[menagerie]> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.01 sec)

[menagerie]> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 102 |
+-----+
2 rows in set (0.00 sec)

客户端 B 插入数据:

[menagerie]> INSERT INTO child (id) VALUES (101);
Query OK, 1 row affected (0.01 sec)

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 101 |
| 102 |
+-----+
3 rows in set (0.00 sec)

客户端 A 在当前事务再次查询,结果不变:

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 102 |
+-----+
2 rows in set (0.00 sec)

客户端 A 结束事务后再次查询,可以看到插入的数据:

[menagerie]> COMMIT;
Query OK, 0 rows affected (0.00 sec)

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 101 |
| 102 |
+-----+
3 rows in set (0.00 sec)

对于 SELECT ... FOR UPDATEUPDATEDELETE 语句:

  • 如果使用唯一索引作为等值查询条件时,InnoDB 只会锁住索引记录。
  • 如果使用其他查询条件,InnoDB 使用间隙锁或者临键锁锁住扫描的索引范围。

READ COMMITTED

对于 SELECT,在同一事务中读取的都是最新的数据。

例如,客户端 A 开启事务执行查询:

[menagerie]> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

[menagerie]> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name         | Value          |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
1 row in set (0.01 sec)

[menagerie]> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 101 |
| 102 |
+-----+
3 rows in set (0.00 sec)

客户端 B 插入数据:

[menagerie]> INSERT INTO child (id) VALUES (100);
Query OK, 1 row affected (0.01 sec)

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 100 |
| 101 |
| 102 |
+-----+
4 rows in set (0.00 sec)

客户端 A 在当前事务再次查询,可以查询到客户端 B 插入的数据:

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 100 |
| 101 |
| 102 |
+-----+
4 rows in set (0.00 sec)

对于 SELECT ... FOR UPDATEUPDATEDELETE 语句,InnoDB 仅锁定索引记录,不锁定它们之前的间隙,可能会出现幻读。间隙锁仅用于外键约束检查和重复键检查。

READ COMMITTED 隔离级别仅支持 row-based 的二进制日志。如果在 READ COMMIT 隔离级别下使用 binlog_format=MIXED ,则将自动使用 row-based 日志记录。

对于 UPDATEDELETE 语句,InnoDB 仅锁定更新或删除的行,大大降低了死锁的可能性。

autocommit, Commit, and Rollback

MySQL 默认启用自动提交(autocommit),每个 SQL 语句作为一个事务,如果语句执行成功,则自动进行提交(commit),如果语句执行失败,根据错误自动进行提交(commit)或回滚(rollback)。

在启用自动提交时,可以使用 START TRANSACTIONBEGIN 语句显示启动一个事务,使用 COMMIT 或者 ROLLBACK 显示结束事务。

如果会话使用 SET autocommit = 0 禁用自动提交,则该会话始终开启一个事务,使用 COMMIT 或者 ROLLBACK 结束该事务并开启一个新事务。

如果会话禁用自动提交,且结束时没有对事务进行显示提交,MySQL 将回滚该事务。

COMMIT 表示在当前事务中所做的更改是永久性的,并且对其他会话可见。ROLLBACK 表示取消当前事务所做的所有修改。COMMIT 和 ROLLBACK 都会释放当前事务期间持有的 InnoDB 锁。

mysql> CREATE TABLE customer (a INT, b CHAR (20), INDEX (a));
Query OK, 0 rows affected (0.00 sec)
mysql> -- Do a transaction with autocommit turned on.
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO customer VALUES (10, 'Heikki');
Query OK, 1 row affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> -- Do another transaction with autocommit turned off.
mysql> SET autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO customer VALUES (15, 'John');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO customer VALUES (20, 'Paul');
Query OK, 1 row affected (0.00 sec)
mysql> DELETE FROM customer WHERE b = 'Heikki';
Query OK, 1 row affected (0.00 sec)
mysql> -- Now we undo those last 2 inserts and the delete.
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM customer;
+------+--------+
| a    | b      |
+------+--------+
|   10 | Heikki |
+------+--------+
1 row in set (0.00 sec)
mysql>

Consistent Nonlocking Reads

一致性读,又称为快照读,表示 InnoDB 使用多版本控制,获取的查询结果为某个时间点的数据库快照。查询获取的是该时间点之前提交的事务所做的更改。但在同一事务中,查询可以获取本事务内先前语句所做的更改。

如果事务隔离级别为默认的 REPEATABLE READ,则同一事务中的所有一致性读都将读取该事务中第一个此类查询建立的快照,参考前面 REPEATABLE READ 的实例。

如果事务隔离级别为 READ COMMITTED,事务中的每个一致性读都会读取最新快照,参考前面 READ COMMITTED 的实例。

一致性读是 InnoDB 在 READ COMMITTED 和 REPEATABLE READ 隔离级别中处理 SELECT 语句的默认模式。一致性读不会在其访问的表上设置任何锁,因此其他会话还可以修改这些表。

假设运行在默认的 REPEATABLE READ 隔离级别,进行一致读(即普通的 SELECT 语句)时,InnoDB 会为事务提供一个时间点,查询该时间点的数据。如果另一个事务删除了一行并在该时间点后提交,则原事务看不到该行已被删除。插入和更新的处理方式类似。

需要注意的是,数据库状态的快照适用于事务中的 SELECT 语句,而不一定适用于 DML 语句。如果插入或修改某些行,然后提交该事务,则从另一个 REPEATABLE READ 事务发出的 DELETE 或 UPDATE 语句可能会影响这些刚刚提交的行,即使会话无法查询到这些行。

例如,客户端 A 开启事务执行查询:

[menagerie]> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.01 sec)

[menagerie]> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 100 |
| 101 |
| 102 |
+-----+
4 rows in set (0.00 sec)

客户端 B 插入数据:

[menagerie]> INSERT INTO child (id) VALUES (99);
Query OK, 1 row affected (0.01 sec)

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
|  99 |
| 100 |
| 101 |
| 102 |
+-----+
5 rows in set (0.00 sec)

客户端 A 在当前事务再次查询,看不到客户端 B 插入的数据,但是插入相同的数据会报错,也可以对客户端 B 插入的数据进行修改,再次查询可以看到修改的数据。

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
| 100 |
| 101 |
| 102 |
+-----+
4 rows in set (0.00 sec)

[menagerie]> INSERT INTO child (id) VALUES (99);
ERROR 1062 (23000): Duplicate entry '99' for key 'child.PRIMARY'
[menagerie]> UPDATE child SET id=98 WHERE id=99;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

[menagerie]> SELECT * FROM CHILD;
+-----+
| id  |
+-----+
|  90 |
|  98 |
| 100 |
| 101 |
| 102 |
+-----+
5 rows in set (0.00 sec)

一致性读不适用于以下 DDL 语句:

  • DROP TABLE
  • ALTER TABLE

对于 SELECT 语句的变种,如未指定 FOR UPDATEFOR SHARE 子句的 INSERT INTO ... SELECTUPDATE ... (SELECT)CREATE TABLE ... SELECT 语句:

  • 默认情况下,InnoDB 对这些语句使用更强的锁,SELECT 部分则表现得像 READ COMMITTED 级别。
  • 要在这种情况下进行无锁读,须将事务隔离级别设置为 READ COMMITTED,以避免读时加锁。

Locking Reads

如果查询数据,然后在同一事务中插入或更新相关数据,则常规 SELECT 语句无法提供足够的保护,因为其他事务可以更新或删除刚刚查询的数据。InnoDB 支持两种类型的锁定读,可提供额外的安全性:

SELECT ... FOR SHARE,对读取的行设置共享模式锁。其他会话可以读取这些行,但在事务提交之前无法修改。如果这些行中的任何一行被尚未提交的另一个事务更改,则查询将等到该事务结束,然后使用最新值。

SELECT ... FOR UPDATE,锁定查询的行和任何关联的索引,就像 UPDATE 这些行一样。其他 UPDATE,SELECT ... FOR SHARE 这些行的事务会被阻塞。

例如,对于 SELECT ... FOR SHARE,在客户端 A 创建父表和子表。

[menagerie]> DROP TABLE parent;
Query OK, 0 rows affected (0.02 sec)

[menagerie]> DROP TABLE child;
Query OK, 0 rows affected (0.02 sec)

[menagerie]> CREATE TABLE parent (
    ->         id INT NOT NULL,
    ->         PRIMARY KEY (id)
    -> )     ENGINE=INNODB;
Query OK, 0 rows affected (0.03 sec)

[menagerie]> CREATE TABLE child (
    ->         id INT,
    ->         parent_id INT,
    ->         INDEX par_ind (parent_id),
    ->         FOREIGN KEY (parent_id)
    ->             REFERENCES parent(id)
    ->             ON UPDATE CASCADE
    ->             ON DELETE CASCADE
    -> )     ENGINE=INNODB;
Query OK, 0 rows affected (0.03 sec)

在客户端 B 为父表插入一条记录:

[menagerie]> INSERT INTO parent VALUES(1);
Query OK, 1 row affected (0.01 sec)

[menagerie]> SELECT * FROM parent;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

在客户端 A 开启事务,查询父表是否有记录:

[menagerie]> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.01 sec)

[menagerie]> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

[menagerie]>  SELECT * FROM parent;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

在客户端 B 删除记录:

[menagerie]> DELETE FROM parent WHERE id=1;
Query OK, 1 row affected (0.02 sec)

[menagerie]> SELECT * FROM parent;
Empty set (0.00 sec)

在客户端 A 向子表插入数据,此时发现本事务内刚刚查询到的父表记录已经不存在了,导致插入失败。

[menagerie]> INSERT INTO child VALUES(1,1);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`menagerie`.`child`, CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`) ON DELETE CASCADE ON UPDATE CASCADE)

[menagerie]> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

为避免此种情形,可以在客户端 A 查询父表记录的时候加上 FOR SHARE:

[menagerie]> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

[menagerie]> SELECT * FROM parent WHERE id=1 FOR SHARE;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

此时,客户端 B 再去删除父表记录则会等待客户端 A 的事务结束。

Locking Read Concurrency with NOWAIT and SKIP LOCKED

SELECT ... FOR UPDATE 或 SELECT ... FOR SHARE 可以使用 NOWAIT 和 SKIP LOCK 选项,避免等待其他事务释放行锁。

  • NOWAIT:使用 NOWAIT 的锁定读从不等待获取行锁。查询将立即执行,如果请求的行被锁定,则失败并返回错误。
  • SKIP LOCKED:使用 SKIP LOCK 的锁定读从不等待获取行锁。查询将立即执行,从结果集中移除锁定的行。

使用 NOWAIT 或 SKIP LOCK 的语句对于基于语句的复制是不安全的。

# Session 1:

mysql> CREATE TABLE t (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;

mysql> INSERT INTO t (i) VALUES(1),(2),(3);

mysql> START TRANSACTION;

mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE;
+---+
| i |
+---+
| 2 |
+---+

# Session 2:

mysql> START TRANSACTION;

mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE NOWAIT;
ERROR 3572 (HY000): Do not wait for lock.

# Session 3:

mysql> START TRANSACTION;

mysql> SELECT * FROM t FOR UPDATE SKIP LOCKED;
+---+
| i |
+---+
| 1 |
| 3 |
+---+

Locks Set by Different SQL Statements in InnoDB

Locking Reads(SELECT ... FOR SHARE 和 SELECT ... FOR UPDATE),UPDATE 以及 DELETE 语句通常会在其扫描的索引范围上加锁,忽略没有用到索引的那部分 WHERE 语句。

如果扫描了二级索引且对二级索引加了排他锁,那么 InnoDB 将会在对应的聚簇索引上加锁。

如果 SQL 语句没有找到合适的索引,则 MySQL 需要扫描整个表,此时表中的所有行将会被锁住,阻塞了其他用户对表的插入。故需要为表创建合适的索引。

InnoDB 中各种 SQL 语句对应的锁:

SELECT ... FROM:一致性读,读取数据库快照,不会加任何锁,除非将事务隔离级别设置为 SERIALIZABLE。

SELECT ... FOR SHARESELECT ... FOR UPDATE:如果使用唯一索引,只获取扫描行的锁。SELECT ... FOR UPDATE 会阻塞其他会话执行 SELECT ... FOR SHARE。

Locking ReadsUPDATE 以及 DELETE:根据语句使用唯一索引进行等值查询还是范围查询,分为:

  • 如果使用唯一索引作为等值查询条件时,InnoDB 只会锁住索引记录。
  • 如果使用其他查询条件,或者使用非唯一索引,InnoDB 使用间隙锁或者临键锁锁住扫描的索引范围。

UPDATE ... WHERE ...:对查询到的索引记录加排他临键锁,对于使用唯一索引的等值查询条件,只加索引记录锁。当 UPDATE 修改聚簇索引记录时,将在相应的二级索引上加上隐式的锁。当进行重复键检测时,将会在插入新的二级索引记录之前,在其二级索引上加共享锁。

DELETE FROM ... WHERE ...:对查询到的索引记录加排他临键锁,对于使用唯一索引的等值查询条件,只加索引记录锁。

INSERT:在插入的记录上加排他锁,此锁是索引记录锁,不是临键锁,不会阻止其他会话在这条记录之前的间隙插入数据。

在插入记录之前,将会加上一种叫做插入意向锁的间隙锁类型。此锁表示插入意向,即插入同一索引间隙的多个事务,如果在间隙中的插入位置不同,则无需相互等待。假设存在值为 4 和 7 的索引记录,两个事务分别插入值 5 和 6,在获得插入行的排他锁之前,每个事务都使用插入意向锁锁定 4 和 7 之间的间隙,但不会相互阻塞。

如果出现重复键错误,则会在重复索引记录上加共享锁。如果有多个会话尝试插入同一行,而另一个会话已经有排他锁,则使用共享锁可能会导致死锁。

假设 InnoDB 表 t1 为:

CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;

三个会话顺序执行以下操作:

会话 1:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

会话 2:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

会话 3:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

会话 1:

ROLLBACK;

会话 1 插入一条记录,没有提交,会在该记录上加上排他锁,会话 2 和会话 3 尝试插入该重复记录,都会被阻塞,会话 2 和会话 3 在该记录上请求共享锁。如果此时会话 1 回滚,将发生死锁。为什么会发生死锁呢?当会话 1 进行回滚时,释放了记录上的排他锁,会话 2 和会话 3 都获得了共享锁,然后会话 2 和会话 3 都想要获得排他锁,进而发生了死锁。

如果表已包含值为 1 的行,并且三个会话顺序执行以下操作,则会出现类似的情况:

会话 1:

START TRANSACTION;
DELETE FROM t1 WHERE i = 1;

会话 2:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

会话 3:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

会话 1:

COMMIT;

会话 1 在该行上加排他锁,会话 2 和会话 3 都插入重复记录,请求行的共享锁。当会话 1 提交,释放排他锁,会话 2 会话 3 获得共享锁,此时出现死锁,因为会话 2 和会话 3 都无法获得排他锁,彼此都持有该记录的共享锁。

INSERT ... ON DUPLICATE KEY UPDATE:和普通的 INSERT 不同,如果碰到重复键,将在记录上加排他锁。对重复的主键加排他索引记录锁,对重复的唯一键加排他临键锁。

REPLACE:在唯一键上没有冲突时和 INSERT 一样,否则将在要替换的记录上加排他临键锁。

INSERT INTO T SELECT ... FROM S WHERE ...:在插入 T 表的每条记录上加排他索引记录锁。如果事务隔离级别为 READ COMMITTED,InnoDB 在 S 表上的查询为一致读(无锁),否则为 S 表上的记录加共享临键锁。

CREATE TABLE ... SELECT ...:与 INSERT ... SELECT 类似。

REPLACE INTO T SELECT ... FROM S WHERE ...UPDATE T ... WHERE col IN (SELECT ... FROM S ...):为 S 表上的行加共享临键锁。

AUTO_INCREMENT:在自增列索引最后加排他锁。

FOREIGN KEY:如果启用了外键约束,任何需要检查约束条件的插入、更新或删除语句都会在相关行上加共享记录锁。InnoDB 还会在约束失败的情况下设置这些锁。

LOCK TABLES:在 MySQL 层加表锁。当 innodb_table_locks = 1(默认)及 autocommit = 0 时,InnoDB 层能感知到表锁,同时 MySQL 层也知晓行级锁。

Phantom Rows

幻读是指在同一个事务中,同样的查询语句,在不同的时间点执行,得到的结果集不同。例如,如果同一个 SELECT 语句执行了两次,第二次执行比第一次执行多出一行,则该行就是所谓的幻影行。

InnoDB 使用临键锁来解决幻读,具体可参考 Transaction Isolation Levelsopen in new window 中 REPEATABLE READ 的示例。

Deadlocks in InnoDB

死锁是因为每个事务都持有另一个事务所需的锁,两个事务都在等待资源变为可用,所以都不会释放它所持有的锁。

为减少死锁,应该:

  • 避免使用 LOCK TABLES 语句。
  • 避免大事务。
  • 不同事务更新多个表或大范围的行时,使用相同的操作顺序。
  • 为 SELECT ... FOR UPDATE 和 UPDATE ... WHERE 用到的列创建索引。

死锁不受隔离级别的影响,因为隔离级别会改变读操作的行为,而死锁是由写操作导致的。

当死锁检测被启用(默认)并且死锁确实发生时,InnoDB 会检测到并回滚其中一个事务。如果使用参数 innodb_deadlock_detect (默认为 ON)禁用死锁检测,那么 InnoDB 将依赖参数 innodb_lock_wait_timeout (默认为 50 秒)在死锁时回滚事务。因此,即使应用程序逻辑正确,仍然需要处理事务重试。

使用 SHOW ENGINE INNODB STATUS 查看 InnoDB 用户事务中的最后一个死锁。启用参数 innodb_print_all_deadlocks 将有关所有死锁的信息打印到错误日志中,以便定位频繁死锁问题。

An InnoDB Deadlock Example

客户端 A 启用参数 innodb_print_all_deadlocks,创建表 Animals 和 Birds,插入数据,然后启动事务,以共享模式查询一条数据:

mysql> SET GLOBAL innodb_print_all_deadlocks = ON;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE Animals (name VARCHAR(10) PRIMARY KEY, value INT) ENGINE = InnoDB;
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE Birds (name VARCHAR(10) PRIMARY KEY, value INT) ENGINE = InnoDB;
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO Animals (name,value) VALUES ("Aardvark",10);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO Birds (name,value) VALUES ("Buzzard",20);
Query OK, 1 row affected (0.00 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT value FROM Animals WHERE name='Aardvark' FOR SHARE;
+-------+
| value |
+-------+
|    10 |
+-------+
1 row in set (0.00 sec)

客户端 B 启动事务,以共享模式查询一条数据:

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT value FROM Birds WHERE name='Buzzard' FOR SHARE;
+-------+
| value |
+-------+
|    20 |
+-------+
1 row in set (0.00 sec)

查看锁信息:

mysql> SELECT ENGINE_TRANSACTION_ID as Trx_Id, 
              OBJECT_NAME as `Table`, 
              INDEX_NAME as `Index`, 
              LOCK_DATA as Data, 
              LOCK_MODE as Mode, 
              LOCK_STATUS as Status, 
              LOCK_TYPE as Type 
        FROM performance_schema.data_locks;
+-----------------+---------+---------+------------+---------------+---------+--------+
| Trx_Id          | Table   | Index   | Data       | Mode          | Status  | Type   |
+-----------------+---------+---------+------------+---------------+---------+--------+
| 421291106147544 | Animals | NULL    | NULL       | IS            | GRANTED | TABLE  |
| 421291106147544 | Animals | PRIMARY | 'Aardvark' | S,REC_NOT_GAP | GRANTED | RECORD |
| 421291106148352 | Birds   | NULL    | NULL       | IS            | GRANTED | TABLE  |
| 421291106148352 | Birds   | PRIMARY | 'Buzzard'  | S,REC_NOT_GAP | GRANTED | RECORD |
+-----------------+---------+---------+------------+---------------+---------+--------+
4 rows in set (0.00 sec)

客户端 B 更新表 Animals 一行数据:

mysql> UPDATE Animals SET value=30 WHERE name='Aardvark';

查询锁信息,客户端 B 处于等待状态:

mysql> SELECT REQUESTING_ENGINE_LOCK_ID as Req_Lock_Id,
              REQUESTING_ENGINE_TRANSACTION_ID as Req_Trx_Id,
              BLOCKING_ENGINE_LOCK_ID as Blk_Lock_Id, 
              BLOCKING_ENGINE_TRANSACTION_ID as Blk_Trx_Id
        FROM performance_schema.data_lock_waits;
+----------------------------------------+------------+----------------------------------------+-----------------+
| Req_Lock_Id                            | Req_Trx_Id | Blk_Lock_Id                            | Blk_Trx_Id      |
+----------------------------------------+------------+----------------------------------------+-----------------+
| 139816129437696:27:4:2:139816016601240 |      43260 | 139816129436888:27:4:2:139816016594720 | 421291106147544 |
+----------------------------------------+------------+----------------------------------------+-----------------+
1 row in set (0.00 sec)

mysql> SELECT ENGINE_LOCK_ID as Lock_Id, 
              ENGINE_TRANSACTION_ID as Trx_id, 
              OBJECT_NAME as `Table`, 
              INDEX_NAME as `Index`, 
              LOCK_DATA as Data, 
              LOCK_MODE as Mode, 
              LOCK_STATUS as Status, 
              LOCK_TYPE as Type 
        FROM performance_schema.data_locks;
+----------------------------------------+-----------------+---------+---------+------------+---------------+---------+--------+
| Lock_Id                                | Trx_Id          | Table   | Index   | Data       | Mode          | Status  | Type   |
+----------------------------------------+-----------------+---------+---------+------------+---------------+---------+--------+
| 139816129437696:1187:139816016603896   |           43260 | Animals | NULL    | NULL       | IX            | GRANTED | TABLE  |
| 139816129437696:1188:139816016603808   |           43260 | Birds   | NULL    | NULL       | IS            | GRANTED | TABLE  |
| 139816129437696:28:4:2:139816016600896 |           43260 | Birds   | PRIMARY | 'Buzzard'  | S,REC_NOT_GAP | GRANTED | RECORD |
| 139816129437696:27:4:2:139816016601240 |           43260 | Animals | PRIMARY | 'Aardvark' | X,REC_NOT_GAP | WAITING | RECORD |
| 139816129436888:1187:139816016597712   | 421291106147544 | Animals | NULL    | NULL       | IS            | GRANTED | TABLE  |
| 139816129436888:27:4:2:139816016594720 | 421291106147544 | Animals | PRIMARY | 'Aardvark' | S,REC_NOT_GAP | GRANTED | RECORD |
+----------------------------------------+-----------------+---------+---------+------------+---------------+---------+--------+
6 rows in set (0.00 sec)

InnoDB 仅在事务试图修改数据库时使用顺序事务 id。因此,以前的只读事务 id 从 421291106148352 更改为 43260。

如果客户端 A 试图同时更新表 Birds 中的一行,将导致死锁:

mysql> UPDATE Birds SET value=40 WHERE name='Buzzard';
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

InnoDB 回滚导致死锁的事务,继续客户端 B 的第一次更新。

死锁数量信息:

mysql> SELECT `count` FROM INFORMATION_SCHEMA.INNODB_METRICS
          WHERE NAME="lock_deadlocks";
+-------+
| count |
+-------+
|     1 |
+-------+
1 row in set (0.00 sec)

死锁和事务信息:

mysql> SHOW ENGINE INNODB STATUS;
------------------------
LATEST DETECTED DEADLOCK
------------------------
2022-11-25 15:58:22 139815661168384
*** (1) TRANSACTION:
TRANSACTION 43260, ACTIVE 186 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 19, OS thread handle 139815619204864, query id 143 localhost u2 updating
UPDATE Animals SET value=30 WHERE name='Aardvark'

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 28 page no 4 n bits 72 index PRIMARY of table `test`.`Birds` trx id 43260 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 7; hex 42757a7a617264; asc Buzzard;;
 1: len 6; hex 00000000a8fb; asc       ;;
 2: len 7; hex 82000000e40110; asc        ;;
 3: len 4; hex 80000014; asc     ;;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `test`.`Animals` trx id 43260 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 8; hex 416172647661726b; asc Aardvark;;
 1: len 6; hex 00000000a8f9; asc       ;;
 2: len 7; hex 82000000e20110; asc        ;;
 3: len 4; hex 8000000a; asc     ;;


*** (2) TRANSACTION:
TRANSACTION 43261, ACTIVE 209 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 18, OS thread handle 139815618148096, query id 146 localhost u1 updating
UPDATE Birds SET value=40 WHERE name='Buzzard'

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `test`.`Animals` trx id 43261 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 8; hex 416172647661726b; asc Aardvark;;
 1: len 6; hex 00000000a8f9; asc       ;;
 2: len 7; hex 82000000e20110; asc        ;;
 3: len 4; hex 8000000a; asc     ;;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 28 page no 4 n bits 72 index PRIMARY of table `test`.`Birds` trx id 43261 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 7; hex 42757a7a617264; asc Buzzard;;
 1: len 6; hex 00000000a8fb; asc       ;;
 2: len 7; hex 82000000e40110; asc        ;;
 3: len 4; hex 80000014; asc     ;;

*** WE ROLL BACK TRANSACTION (2)
------------
TRANSACTIONS
------------
Trx id counter 43262
Purge done for trx's n:o < 43256 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421291106147544, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 421291106146736, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 421291106145928, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 43260, ACTIVE 219 sec
4 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 19, OS thread handle 139815619204864, query id 143 localhost u2

错误日志中的死锁和事务信息:

mysql> SELECT @@log_error;
+---------------------+
| @@log_error         |
+---------------------+
| /var/log/mysqld.log |
+---------------------+
1 row in set (0.00 sec)

TRANSACTION 43260, ACTIVE 186 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 19, OS thread handle 139815619204864, query id 143 localhost u2 updating
UPDATE Animals SET value=30 WHERE name='Aardvark'
RECORD LOCKS space id 28 page no 4 n bits 72 index PRIMARY of table `test`.`Birds` trx id 43260 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 7; hex 42757a7a617264; asc Buzzard;;
 1: len 6; hex 00000000a8fb; asc       ;;
 2: len 7; hex 82000000e40110; asc        ;;
 3: len 4; hex 80000014; asc     ;;

RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `test`.`Animals` trx id 43260 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 8; hex 416172647661726b; asc Aardvark;;
 1: len 6; hex 00000000a8f9; asc       ;;
 2: len 7; hex 82000000e20110; asc        ;;
 3: len 4; hex 8000000a; asc     ;;

TRANSACTION 43261, ACTIVE 209 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 18, OS thread handle 139815618148096, query id 146 localhost u1 updating
UPDATE Birds SET value=40 WHERE name='Buzzard'
RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `test`.`Animals` trx id 43261 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 8; hex 416172647661726b; asc Aardvark;;
 1: len 6; hex 00000000a8f9; asc       ;;
 2: len 7; hex 82000000e20110; asc        ;;
 3: len 4; hex 8000000a; asc     ;;

RECORD LOCKS space id 28 page no 4 n bits 72 index PRIMARY of table `test`.`Birds` trx id 43261 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 7; hex 42757a7a617264; asc Buzzard;;
 1: len 6; hex 00000000a8fb; asc       ;;
 2: len 7; hex 82000000e40110; asc        ;;
 3: len 4; hex 80000014; asc     ;;

Deadlock Detection

当启用死锁检测(默认)时,InnoDB 会自动检测事务死锁,并回滚一个或多个事务以跳出死锁。InnoDB 尝试选择小事务进行回滚,其中事务的大小由插入、更新或删除的行数决定。

innodb_table_locks = 1(默认)及 autocommit = 0 时,InnoDB 层能感知到表锁,同时 MySQL 层也知晓行级锁。否则,InnoDB 无法检测到涉及由 MySQL LOCK TABLES 语句设置的表锁或由 InnoDB 以外的存储引擎设置的锁的死锁,可以通过设置参数 innodb_lock_wait_timeout 来解决。

如果 InnoDB Monitor 输出的 “LATEST DETECTED DEADLOCK” 部分包含一条消息为:“TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION”,表明等待列表中的事务数已达到 200 限制。超过 200 个事务的等待列表将被视为死锁,尝试检查等待列表的事务将被回滚。如果锁定线程必须查看等待列表上事务所拥有的 1000000 个以上的锁,也可能发生相同的错误。

在高并发系统,当多个线程等待同一个锁时,死锁检测可能会导致性能变差。有时,当死锁发生时,禁用死锁检测并依靠 innodb_lock_wait_timeout 设置进行事务回滚可能会更高效。可以使用参数 innodb_deadlock_detect 禁用死锁检测。

How to Minimize and Handle Deadlocks

InnoDB 使用自动行级锁。即使在只插入或删除一行的事务,也可能出现死锁。因为这些操作并不是真正的 “原子” 操作,会自动对插入或删除的行的索引记录加锁。

可以使用以下技术来处理死锁并降低其发生的可能性:

  • 使用 SHOW ENGINE INNODB STATUS 确定最后死锁的原因,调优应用,避免死锁。
  • 启用参数 innodb_print_all_deadlocks 将所有死锁的信息打印到错误日志中,以便定位频繁死锁问题。
  • 如果事务由于死锁而失败,做好重试机制。
  • 避免大事务。
  • 尽快提交事务。
  • 如果使用 Locking Reads(SELECT ... FOR SHARE 和 SELECT ... FOR UPDATE),使用 READ COMMITTED 隔离级别。
  • 不同事务更新多个表或大范围的行时,使用相同的操作顺序。
  • 使用合适的索引。
  • 通过序列化事务来解决,一种方式是加表锁,步骤如下:
SET autocommit=0;
LOCK TABLES t1 WRITE, t2 READ, ...;
... do something with tables t1 and t2 here ...
COMMIT;
UNLOCK TABLES;
  • 另一种序列化事务方法是创建一个只包含一行的辅助表。在访问其他表之前,让每个事务更新该行。通过这种方式,所有事务都以串行方式发生。

Transaction Scheduling

InnoDB 使用 Contention-Aware Transaction Scheduling(CATS)算法对等待锁的事务进行优先级排序。当多个事务等待同一对象上的锁时,CATS 算法会确定哪个事务先获得锁。

CATS 算法通过分配调度权重来对等待的事务进行优先级排序,调度权重是根据事务阻塞的事务数量来计算。例如,两个事务正在等待同一对象的锁,那么阻塞最多事务的事务将被分配更大的调度权重。如果权重相等,则优先考虑等待时间最长的事务。

在 MySQL 8.0.20 之前,InnoDB 还使用 First In First Out(FIFO)算法来调度事务,CATS 算法仅用于锁争用严重情况。

查看事务调度权重:

[(none)]> SELECT trx_id,trx_state,trx_schedule_weight FROM information_schema.INNODB_TRX;
+--------+-----------+---------------------+
| trx_id | trx_state | trx_schedule_weight |
+--------+-----------+---------------------+
|  27923 | LOCK WAIT |                   1 |
|  27922 | RUNNING   |                NULL |
+--------+-----------+---------------------+
2 rows in set (0.00 sec)

InnoDB Configuration

InnoDB Startup Configuration

在初始化 InnoDB 之前,应配置好数据文件、日志文件、页大小和内存缓冲区。

Specifying Options in a MySQL Option File

通常情况下,在 MySQL Server 第一次启动时初始化 InnoDB,故在 MySQL 启动之前,在参数文件中配置数据文件、日志文件、页大小,则在 MySQL 启动时,读取这些参数来初始化 InnoDB。

在参数文件的 [mysqld] 组中设置 InnoDB 参数,详细的参数文件说明请参考:Using Option Filesopen in new window

Important Storage Considerations

  • 在某些情况下,可以通过将数据文件和日志文件放在不同的物理磁盘上来提高数据库性能。还可以为 InnoDB 数据文件使用裸磁盘分区,提高 I/O 速度。
  • 某些操作系统或磁盘子系统可能会延迟或重新排序写入操作以提高性能。操作系统崩溃或停电可能会破坏最近提交的数据,或者在最坏的情况下,会因为写入操作被重新排序而损坏数据库,可以通过断电来进行测试。在 Linux 环境,如果使用的是 ATA/SATA 磁盘,建议使用命令 hdparm -W0 /dev/hda 禁用回写缓存。需要注意的是,某些驱动器或磁盘控制器可能无法禁用回写缓存。
  • InnoDB 默认启用双写缓冲区来保护用户数据,提高性能,参考:Doublewrite Bufferopen in new window

System Tablespace Data File Configuration

参数 innodb_data_file_path 指定 InnoDB 系统表空间数据文件名称、大小和属性。如果不配置,则默认创建一个名称为 ibdata1,初始大小为 12M,自动扩展的数据文件。

mysql> SHOW VARIABLES LIKE 'innodb_data_file_path';
+-----------------------+------------------------+
| Variable_name         | Value                  |
+-----------------------+------------------------+
| innodb_data_file_path | ibdata1:12M:autoextend |
+-----------------------+------------------------+

语法:

file_name:file_size[:autoextend[:max:max_file_size]]

可以指定多个数据文件,以分号隔开:

[mysqld]
innodb_data_file_path=ibdata1:50M;ibdata2:50M:autoextend

autoextendmax 属性只能用于最后一个数据文件。

[mysqld]
innodb_data_file_path=ibdata1:12M:autoextend:max:500M

数据文件默认位于数据目录下(参数 datadir 指定),也可以使用参数 innodb_data_home_dir 指定:

[mysqld]
innodb_data_home_dir = /myibdata/
innodb_data_file_path=ibdata1:50M:autoextend

也可以为数据文件指定绝对路径:

[mysqld]
innodb_data_file_path=/myibdata/ibdata1:50M:autoextend

参考:The System Tablespaceopen in new window

InnoDB Doublewrite Buffer File Configuration

在 MySQL 8.0.20 之前,双写缓冲区位于 InnoDB 系统表空间中。从 MySQL 8.0.20 开始,双写缓冲区位于双写文件中。具体配置参考:Doublewrite Bufferopen in new window

Redo Log Configuration

从 MySQL 8.0.30 开始,参数 innodb_redo_log_capacity 控制重做日志文件占用的磁盘空间。具体配置参考:Redo Logopen in new window

Undo Tablespace Configuration

默认 UNDO 表空间创建在参数 innodb_undo_directory 指定的位置。 UNDO 日志的 I/O 模式使得 UNDO 表空间非常适合 SSD 存储。具体配置参考:Undo Tablespacesopen in new window

Global Temporary Tablespace Configuration

全局临时表空间存储对用户创建的临时表所做更改的回滚段。具体配置参考:Global Temporary Tablespaceopen in new window

Session Temporary Tablespace Configuration

会话临时表空间存储用户创建的临时表和优化器创建的内部临时表。具体配置参考:Session Temporary Tablespacesopen in new window

Page Size Configuration

参数 innodb_page_size 用于在初始化 MySQL 实例时指定 InnoDB 表空间页大小,默认是 16 KB,适用于大多数工作负载。

Memory Configuration

使用以下参数进行配置 InnoDB 的缓冲区:

使用参数 innodb_buffer_pool_size 指定缓冲池大小,建议将 innodb_buffer_pool_size 配置为系统内存的 50% 到 75%。默认缓冲池大小为128MB。在大内存系统上,可以通过将缓冲池划分为多个缓冲池实例来提高并发,缓冲池实例的数量由参数 innodb_buffer_pool_instances 指定。默认情况下,InnoDB 创建一个缓冲池实例。可以在启动时配置缓冲池实例的数量。

使用参数 innodb_log_buffer_size 指定日志缓冲区大小,默认为 16MB。具体配置参考:Log Bufferopen in new window

使用参数 global_connection_memory_limit 指定所有用户连接使用的内存总量,状态变量 Global_connection_memory 不能超过该参数值,否则普通用户查询报错 ER_GLOBAL_CONN_LIMIT,root 用户不会报错。类似于 Oracle 数据库的参数 PGA_AGGREGATE_TARGET

使用参数 connection_memory_limit 指定单个普通用户连接使用的最大内存,超过此值报错 ER_CONN_LIMIT,root 用户不会报错。

使用参数 connection_memory_chunk_size 指定普通用户连接使用内存的统计更新频率,默认为 8KB,也就是当内存使用变化超过 8KB 时,才会更新统计结果到状态变量 Global_connection_memory

使用参数 global_connection_memory_tracking (默认为 OFF)启用对用户连接消耗内存的统计,开启后才会将用户连接使用内存写入到状态变量 Global_connection_memory 。可以全局开启,也可以在单个会话中独立开启。如果是全局开启,则会针对所有连接统计内存消耗情况,包括系统内部线程,以及 root 用户创建的连接;如果是在单个会话中独立开启,则只会统计当前会话连接的内存消耗。此外,InnoDB Buffer Pool 不在统计范围内。

假设服务器物理内存为 64GB,建议如下初始配置:

参数设置值
innodb_buffer_pool_size32G
global_connection_memory_limit16G
connection_memory_chunk_size8192
connection_memory_limit64M
global_connection_memory_trackingON

在上述配置中,设置了每个会话中普通用户执行的 SQL 消耗内存不能超过 64MB,所有会话消耗的内存总量不超过 16GB,至少可支撑 256 个并发连接;此外,Innodb Buffer Pool + 各会话内存的和是 48G,为物理内存的 75%,已给系统预留出充足的剩余内存,降低发生 SWAP 的风险。

InnoDB Buffer Pool Configuration

Configuring InnoDB Buffer Pool Size

可以停机或联机配置 InnoDB 缓冲池大小。

以 chunk 为单位调整 innodb_buffer_pool_size ,参数 innodb_buffer_pool_chunk_size 指定chunk 大小,默认为 128M。

缓冲池大小必须等于或者倍于 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances ,如果配置的 innodb_buffer_pool_size 不符合这个公式,则会自动调整缓冲池大小以符合。

例如,设置 innodb_buffer_pool_size 为 8G,innodb_buffer_pool_instances 为 16, innodb_buffer_pool_chunk_size 为 128M(默认值)。因为 8G 是 16 * 128M 的倍数,故为符合公式的有效值。

$> mysqld --innodb-buffer-pool-size=8G --innodb-buffer-pool-instances=16
mysql> SELECT @@innodb_buffer_pool_size/1024/1024/1024;
+------------------------------------------+
| @@innodb_buffer_pool_size/1024/1024/1024 |
+------------------------------------------+
|                           8.000000000000 |
+------------------------------------------+

例如,设置 innodb_buffer_pool_size 为 9G,innodb_buffer_pool_instances 为 16, innodb_buffer_pool_chunk_size 为 128M(默认值)。因为 9G 不是 16 * 128M 的倍数,不符合公式,会自动调整为符合公式的有效值 10G。

$> mysqld --innodb-buffer-pool-size=9G --innodb-buffer-pool-instances=16
mysql> SELECT @@innodb_buffer_pool_size/1024/1024/1024;
+------------------------------------------+
| @@innodb_buffer_pool_size/1024/1024/1024 |
+------------------------------------------+
|                          10.000000000000 |
+------------------------------------------+
Configuring InnoDB Buffer Pool Chunk Size

只能在启动的时候,使用命令行字符串或者参数文件,以 1M(1048576 byte)为单位增大或减少 innodb_buffer_pool_chunk_size

命令行:

$> mysqld --innodb-buffer-pool-chunk-size=134217728

参数文件:

[mysqld]
innodb_buffer_pool_chunk_size=134217728

在初始化缓冲池时,新的 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances 大于当前的缓冲池,则会将 innodb_buffer_pool_chunk_size 截断为 innodb_buffer_pool_size / innodb_buffer_pool_instances

例如,如果缓冲池初始化为 2G(2147483648 bytes),4 个缓冲池实例,chunk 为 1G(1073741824 bytes),则将 chunk 截断为 2147483648 / 4

$> mysqld --innodb-buffer-pool-size=2147483648 --innodb-buffer-pool-instances=4
--innodb-buffer-pool-chunk-size=1073741824;
mysql> SELECT @@innodb_buffer_pool_size;
+---------------------------+
| @@innodb_buffer_pool_size |
+---------------------------+
|                2147483648 |
+---------------------------+

mysql> SELECT @@innodb_buffer_pool_instances;
+--------------------------------+
| @@innodb_buffer_pool_instances |
+--------------------------------+
|                              4 |
+--------------------------------+

# Chunk size was set to 1GB (1073741824 bytes) on startup but was
# truncated to innodb_buffer_pool_size / innodb_buffer_pool_instances

mysql> SELECT @@innodb_buffer_pool_chunk_size;
+---------------------------------+
| @@innodb_buffer_pool_chunk_size |
+---------------------------------+
|                       536870912 |
+---------------------------------+

如果修改了 innodb_buffer_pool_chunk_size,会在初始化缓冲池时自动向上调整 innodb_buffer_pool_size 为等于或者倍于 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances 。例如:

# The buffer pool has a default size of 128MB (134217728 bytes)

mysql> SELECT @@innodb_buffer_pool_size;
+---------------------------+
| @@innodb_buffer_pool_size |
+---------------------------+
|                 134217728 |
+---------------------------+

# The chunk size is also 128MB (134217728 bytes)

mysql> SELECT @@innodb_buffer_pool_chunk_size;
+---------------------------------+
| @@innodb_buffer_pool_chunk_size |
+---------------------------------+
|                       134217728 |
+---------------------------------+

# There is a single buffer pool instance

mysql> SELECT @@innodb_buffer_pool_instances;
+--------------------------------+
| @@innodb_buffer_pool_instances |
+--------------------------------+
|                              1 |
+--------------------------------+

# Chunk size is decreased by 1MB (1048576 bytes) at startup
# (134217728 - 1048576 = 133169152):

$> mysqld --innodb-buffer-pool-chunk-size=133169152

mysql> SELECT @@innodb_buffer_pool_chunk_size;
+---------------------------------+
| @@innodb_buffer_pool_chunk_size |
+---------------------------------+
|                       133169152 |
+---------------------------------+

# Buffer pool size increases from 134217728 to 266338304
# Buffer pool size is automatically adjusted to a value that is equal to
# or a multiple of innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances

mysql> SELECT @@innodb_buffer_pool_size;
+---------------------------+
| @@innodb_buffer_pool_size |
+---------------------------+
|                 266338304 |
+---------------------------+
# The buffer pool has a default size of 2GB (2147483648 bytes)

mysql> SELECT @@innodb_buffer_pool_size;
+---------------------------+
| @@innodb_buffer_pool_size |
+---------------------------+
|                2147483648 |
+---------------------------+

# The chunk size is .5 GB (536870912 bytes)

mysql> SELECT @@innodb_buffer_pool_chunk_size;
+---------------------------------+
| @@innodb_buffer_pool_chunk_size |
+---------------------------------+
|                       536870912 |
+---------------------------------+

# There are 4 buffer pool instances

mysql> SELECT @@innodb_buffer_pool_instances;
+--------------------------------+
| @@innodb_buffer_pool_instances |
+--------------------------------+
|                              4 |
+--------------------------------+

# Chunk size is decreased by 1MB (1048576 bytes) at startup
# (536870912 - 1048576 = 535822336):

$> mysqld --innodb-buffer-pool-chunk-size=535822336

mysql> SELECT @@innodb_buffer_pool_chunk_size;
+---------------------------------+
| @@innodb_buffer_pool_chunk_size |
+---------------------------------+
|                       535822336 |
+---------------------------------+

# Buffer pool size increases from 2147483648 to 4286578688
# Buffer pool size is automatically adjusted to a value that is equal to
# or a multiple of innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances

mysql> SELECT @@innodb_buffer_pool_size;
+---------------------------+
| @@innodb_buffer_pool_size |
+---------------------------+
|                4286578688 |
+---------------------------+

可以看到修改了 innodb_buffer_pool_chunk_size 会增大缓冲池,因此在修改之前需要评估修改后的缓冲池大小是否合适。

注意:为避免潜在的性能问题,chunk 数量(innodb_buffer_pool_size / innodb_buffer_pool_chunk_size)不应超过 1000。

Configuring InnoDB Buffer Pool Size Online

可以使用 SET 语句动态修改参数 innodb_buffer_pool_size 而不需要重启,例如:

mysql> SET GLOBAL innodb_buffer_pool_size=402653184;

执行语句后,需要等待所有活动事务都完成后,才会开始调整。调整进行时,需要访问缓冲池的新事务和操作必须等待,直到调整完成。

Monitoring Online Buffer Pool Resizing Progress

使用状态变量 InnoDB_buffer_pool_resize_status 查看缓冲池调整的进度。

mysql> SHOW STATUS WHERE Variable_name='InnoDB_buffer_pool_resize_status';
+----------------------------------+----------------------------------+
| Variable_name                    | Value                            |
+----------------------------------+----------------------------------+
| Innodb_buffer_pool_resize_status | Resizing also other hash tables. |
+----------------------------------+----------------------------------+

从 MyQL 8.0.31 开始,还可以使用 Innodb_buffer_pool_resize_status_codeInnodb_buffer_pool_resize_status_progress 状态变量监视缓冲池调整操作。

Innodb_buffer_pool_resize_status_code 的状态码表示的调整阶段有:

  • 0: No Resize operation in progress
  • 1: Starting Resize
  • 2: Disabling AHI (Adaptive Hash Index)
  • 3: Withdrawing Blocks
  • 4: Acquiring Global Lock
  • 5: Resizing Pool
  • 6: Resizing Hash
  • 7: Resizing Failed

Innodb_buffer_pool_resize_status_progress 显示每个阶段的进度百分比。

还可以使用以下 SQL 查询:

SELECT variable_name, variable_value 
 FROM performance_schema.global_status 
 WHERE LOWER(variable_name) LIKE "innodb_buffer_pool_resize%";

可以在错误日志中查看调整进度。例如,增加缓冲池的日志:

[Note] InnoDB: Resizing buffer pool from 134217728 to 4294967296. (unit=134217728)
[Note] InnoDB: disabled adaptive hash index.
[Note] InnoDB: buffer pool 0 : 31 chunks (253952 blocks) was added.
[Note] InnoDB: buffer pool 0 : hash tables were resized.
[Note] InnoDB: Resized hash tables at lock_sys, adaptive hash index, dictionary.
[Note] InnoDB: completed to resize buffer pool from 134217728 to 4294967296.
[Note] InnoDB: re-enabled adaptive hash index.

例如,减少缓冲池的日志:

[Note] InnoDB: Resizing buffer pool from 4294967296 to 134217728. (unit=134217728)
[Note] InnoDB: disabled adaptive hash index.
[Note] InnoDB: buffer pool 0 : start to withdraw the last 253952 blocks.
[Note] InnoDB: buffer pool 0 : withdrew 253952 blocks from free list. tried to relocate 
0 pages. (253952/253952)
[Note] InnoDB: buffer pool 0 : withdrawn target 253952 blocks.
[Note] InnoDB: buffer pool 0 : 31 chunks (253952 blocks) was freed.
[Note] InnoDB: buffer pool 0 : hash tables were resized.
[Note] InnoDB: Resized hash tables at lock_sys, adaptive hash index, dictionary.
[Note] InnoDB: completed to resize buffer pool from 4294967296 to 134217728.
[Note] InnoDB: re-enabled adaptive hash index.

从 MySQL 8.0.31 开始,使用 --log-error-verbosity=3 启动会在调整缓冲池期间将 Innodb_buffer_pool_resize_status_codeInnodb_buffer_pool_resize_status_progress 记录到错误日志。

[Note] [MY-012398] [InnoDB] Requested to resize buffer pool. (new size: 1073741824 bytes)
[Note] [MY-013954] [InnoDB] Status code 1: Resizing buffer pool from 134217728 to 1073741824
(unit=134217728).
[Note] [MY-013953] [InnoDB] Status code 1: 100% complete
[Note] [MY-013952] [InnoDB] Status code 1: Completed
[Note] [MY-013954] [InnoDB] Status code 2: Disabling adaptive hash index.
[Note] [MY-011885] [InnoDB] disabled adaptive hash index.
[Note] [MY-013953] [InnoDB] Status code 2: 100% complete
[Note] [MY-013952] [InnoDB] Status code 2: Completed
[Note] [MY-013954] [InnoDB] Status code 3: Withdrawing blocks to be shrunken.
[Note] [MY-013953] [InnoDB] Status code 3: 100% complete
[Note] [MY-013952] [InnoDB] Status code 3: Completed
[Note] [MY-013954] [InnoDB] Status code 4: Latching whole of buffer pool.
[Note] [MY-013953] [InnoDB] Status code 4: 14% complete
[Note] [MY-013953] [InnoDB] Status code 4: 28% complete
[Note] [MY-013953] [InnoDB] Status code 4: 42% complete
[Note] [MY-013953] [InnoDB] Status code 4: 57% complete
[Note] [MY-013953] [InnoDB] Status code 4: 71% complete
[Note] [MY-013953] [InnoDB] Status code 4: 85% complete
[Note] [MY-013953] [InnoDB] Status code 4: 100% complete
[Note] [MY-013952] [InnoDB] Status code 4: Completed
[Note] [MY-013954] [InnoDB] Status code 5: Starting pool resize
[Note] [MY-013954] [InnoDB] Status code 5: buffer pool 0 : resizing with chunks 1 to 8.
[Note] [MY-011891] [InnoDB] buffer pool 0 : 7 chunks (57339 blocks) were added.
[Note] [MY-013953] [InnoDB] Status code 5: 100% complete
[Note] [MY-013952] [InnoDB] Status code 5: Completed
[Note] [MY-013954] [InnoDB] Status code 6: Resizing hash tables.
[Note] [MY-011892] [InnoDB] buffer pool 0 : hash tables were resized.
[Note] [MY-013953] [InnoDB] Status code 6: 100% complete
[Note] [MY-013954] [InnoDB] Status code 6: Resizing also other hash tables.
[Note] [MY-011893] [InnoDB] Resized hash tables at lock_sys, adaptive hash index, dictionary.
[Note] [MY-011894] [InnoDB] Completed to resize buffer pool from 134217728 to 1073741824.
[Note] [MY-011895] [InnoDB] Re-enabled adaptive hash index.
[Note] [MY-013952] [InnoDB] Status code 6: Completed
[Note] [MY-013954] [InnoDB] Status code 0: Completed resizing buffer pool at 220826  6:25:46.
[Note] [MY-013953] [InnoDB] Status code 0: 100% complete

Configuring Multiple Buffer Pool Instances

对于有大量内存的服务器,可以将缓冲池划分为多个实例,减少不同线程读取和写入缓存页时的争用,从而提高并发性能。使用参数 innodb_buffer_pool_instances 配置多个缓冲池实例,默认为 1,最大为 64,只有在参数 innodb_buffer_pool_size 不小于 1G 时才生效。故基于性能考虑,每个缓冲池实例至少 1G。

[(none)]> SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_buffer_pool_instances | 8     |
+------------------------------+-------+
1 row in set (0.01 sec)

Making the Buffer Pool Scan Resistant

InnoDB 没有使用严格的 LRU 算法,而是使用了一种技术来最大限度地减少被带入缓冲池且不再被访问的数据量(类似于没有 WHERE 条件的 SELECT 语句或者进行 mysqldump 操作),以确保将频繁访问的页保留在缓冲池中。

InnoDB 的 LRU 算法参考:Buffer Pool LRU Algorithmopen in new window

使用参数 innodb_old_blocks_pct 控制 LRU 列表中旧子列表的百分比,默认为 37(3/8),范围为 5 到 95。

[(none)]> SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37    |
+-----------------------+-------+
1 row in set (0.02 sec)

使用参数 innodb_old_blocks_time 指定插入到旧子列表中的块在第一次访问后必须停留在那里的时间(以毫秒为单位),然后才能移动到新子列表。默认值为 1000,增大这个值会使越来越多的块从缓冲池中更快地老化。

[(none)]> SHOW VARIABLES LIKE 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000  |
+------------------------+-------+
1 row in set (0.01 sec)

参数 innodb_old_blocks_pctinnodb_old_blocks_time 都可以在参数文件中指定,也可以在运行时使用 SET GLOBAL 语句进行更改。

使用 SHOW ENGINE INNODB STATUS 命令查看修改后的效果。

当扫描无法完全放入缓冲池的大表时,将 innodb_old_blocks_pct 设置为较小的值可以防止只读取一次的数据占用缓冲池的很大一部分。

当扫描可以完全放入缓冲池的小表时,可以将 innodb_old_blocks_pct 保留为默认值。

Configuring InnoDB Buffer Pool Prefetching (Read-Ahead)

预读请求是一种 I/O 请求,异步预取一整个区的页到缓冲池,以应对即将出现的对这些页的需求。InnoDB 使用两种预读算法来提高 I/O 性能:

Linear read-ahead(线性预读):是一种基于按顺序访问的缓冲池中的页来预测可能很快需要访问哪些页的技术。使用参数 innodb_read_ahead_threshold,配置触发异步读取请求所需的顺序页访问次数,来控制 InnoDB 执行预读操作的时间。默认值为 56,范围为 0 到 64。例如,如果将该值设置为48,则只有在当前区中有48个页被顺序访问时,InnoDB 才会触发线性预读请求。可以在 MySQL 参数文件中设置此参数,也可以使用 SET GLOBAL 语句动态更改。

Random read-ahead(随机预读):是一种技术,根据缓冲池中的页预测何时可能需要页,而不考虑这些页的读取顺序。如果在缓冲池中发现同一个区的 13 个连续页,InnoDB 会异步发出一个请求来预取该区的剩余页。通过配置参数 innodb_random_read_aheadON (默认为 OFF)来启用随机预读。

使用 SHOW ENGINE INNODB STATUS 命令查看预读算法的统计信息。也可以查看以下状态变量:

  • Innodb_buffer_pool_read_ahead:预读后台线程读取到 InnoDB 缓冲池的页数。

  • Innodb_buffer_pool_read_ahead_evicted:预读后台线程读取到 InnoDB 缓冲池后,没有被查询访问而被驱逐的页数。

  • Innodb_buffer_pool_read_ahead_rnd:随机预读页数。

[(none)]> SHOW GLOBAL STATUS LIKE '%ahead%';
+---------------------------------------+-------+
| Variable_name                         | Value |
+---------------------------------------+-------+
| Innodb_buffer_pool_read_ahead_rnd     | 0     |
| Innodb_buffer_pool_read_ahead         | 0     |
| Innodb_buffer_pool_read_ahead_evicted | 0     |
+---------------------------------------+-------+
3 rows in set (0.01 sec)

Configuring Buffer Pool Flushing

脏页是指已在内存中修改但尚未写入到磁盘上数据文件的页。

在 MySQL 8.0 中,由页面清理线程将缓冲池脏页刷新到磁盘。参数 innodb_page_cleaners 指定页面清理线程数量,默认为 4。如果超过了缓冲池实例数量,则自动设置为 innodb_buffer_pool_instances

[(none)]> SHOW VARIABLES LIKE 'innodb_page_cleaners';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| innodb_page_cleaners | 4     |
+----------------------+-------+
1 row in set (0.01 sec)

当脏页的百分比达到参数 innodb_max_dirty_pages_pct_lwm 定义的低水位线时,启动缓冲池刷新。默认为 10。设置为 0 将禁用此提前刷新行为。

[(none)]> SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct_lwm';
+--------------------------------+-----------+
| Variable_name                  | Value     |
+--------------------------------+-----------+
| innodb_max_dirty_pages_pct_lwm | 10.000000 |
+--------------------------------+-----------+
1 row in set (0.00 sec)

设置参数 innodb_max_dirty_pages_pct_lwm 是为了防止脏页的数量达到参数 innodb_max_dirty_pages_pct 定义的百分比阈值,默认为 90。如果脏页的百分比达到此阈值,InnoDB 会主动刷新缓冲池页面。

[(none)]> SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';
+----------------------------+-----------+
| Variable_name              | Value     |
+----------------------------+-----------+
| innodb_max_dirty_pages_pct | 90.000000 |
+----------------------------+-----------+
1 row in set (0.00 sec)

参数 innodb_max_dirty_pages_pct_lwm 应始终小于参数 innodb_max_dirty_pages_pct

其他对缓冲池刷新行为进行微调的参数有:

innodb_flush_neighbors:指定从缓冲池中刷新页的时候,是否也会刷新同区内的其他脏页。 可以设置的值有:

  • 0:默认值,不会刷新同区内的脏页,推荐为 SSD 磁盘设置此值。
  • 1:刷新同区内连续的脏页。
  • 2:刷新同区内的脏页。
[(none)]> SHOW VARIABLES LIKE 'innodb_flush_neighbors';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_flush_neighbors | 0     |
+------------------------+-------+
1 row in set (0.00 sec)

innodb_lru_scan_depth:为每个缓冲池实例指定页面清理器线程在缓冲池 LRU 列表下扫描脏页的深度,这是页面清理线程每秒执行一次的后台操作。默认值为 1024,适合于大多数工作负载。只有在典型工作负载下有空闲 I/O 时,才考虑增大该值。如果写密集型工作负载使 I/O 饱和,考虑减小该值,尤其是在缓冲池很大的情况下。

[(none)]> SHOW VARIABLES LIKE 'innodb_lru_scan_depth';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_lru_scan_depth | 1024  |
+-----------------------+-------+
1 row in set (0.01 sec)

根据工作负载、数据访问模式和存储配置来设置以上参数。

Adaptive Flushing

InnoDB 使用自适应刷新算法,根据重做日志生成的速度和当前刷新率动态调整刷新率。其目的是通过确保刷新活动与当前工作负载保持同步来平滑整体性能。

参数 innodb_adaptive_flushing_lwm 定义了重做日志容量的低水位线百分比,默认为 10,当超过该阈值时,即使参数 innodb_adaptive_flushing 被禁用,也会启用自适应刷新。

[(none)]> SHOW VARIABLES LIKE 'innodb_adaptive_flushing_lwm';
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_adaptive_flushing_lwm | 10    |
+------------------------------+-------+
1 row in set (0.01 sec)

如果自适应刷新不适合当前工作负载特征,可以使用参数 innodb_adaptive_flushing 禁用,默认为启用。

[(none)]> SHOW VARIABLES LIKE 'innodb_adaptive_flushing';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_adaptive_flushing | ON    |
+--------------------------+-------+
1 row in set (0.00 sec)

参数 innodb_flushing_avg_loops 定义了 InnoDB 保留先前计算的刷新状态快照的迭代次数,控制自适应刷新对前台工作负载变化的响应速度。默认为 30,范围为 1 到 1000。增大该值意味着 InnoDB 会使之前计算的快照保留更长时间,因此自适应刷新的响应更慢。

[(none)]> SHOW VARIABLES LIKE 'innodb_flushing_avg_loops';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| innodb_flushing_avg_loops | 30    |
+---------------------------+-------+
1 row in set (0.00 sec)

刷新的速率可以超过参数 innodb_io_capacity 定义的 I/O 速率,默认为 200 IOPS,但不能超过参数 innodb_io_capacity_max 指定的最大 I/O 速率。

[(none)]> SHOW VARIABLES LIKE 'innodb_io_capacity%';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_io_capacity     | 200   |
| innodb_io_capacity_max | 2000  |
+------------------------+-------+
2 rows in set (0.01 sec)
Limiting Buffer Flushing During Idle Periods

从 MySQL 8.0.18 开始,可以使用 innodb_idle_flush_pct 参数限制空闲时间(没有页被修改)缓冲池的刷新速率。默认为 100,表示 100% 使用 innodb_io_capacity 的 IOPS。通过设置小于 100 的值来限制空闲时间的刷新,以延长 SSD 的使用寿命。

Saving and Restoring the Buffer Pool State

为了缩短重新启动服务器后的预热时间,InnoDB 在服务器关闭时为每个缓冲池保存一定比例的最近使用的页,并在服务器启动时恢复这些页。参数 innodb_buffer_pool_dump_pct 指定最近使用的页的百分比,默认为 25。

[(none)]> SHOW VARIABLES LIKE 'innodb_buffer_pool_dump_pct';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| innodb_buffer_pool_dump_pct | 25    |
+-----------------------------+-------+
1 row in set (0.01 sec)

InnoDB 保存到磁盘上的缓冲池数据很小,包含表空间 ID 和页面 ID,位于数据目录下,参数 innodb_buffer_pool_filename 指定文件名,默认为 ib_buffer_pool。

[root@mysql ~]# ll /data/mysql/ib_buffer_pool 
-rw-r----- 1 mysql mysql 3935 Mar 25 11:40 /data/mysql/ib_buffer_pool
[(none)]> SHOW VARIABLES LIKE 'innodb_buffer_pool_filename';
+-----------------------------+----------------+
| Variable_name               | Value          |
+-----------------------------+----------------+
| innodb_buffer_pool_filename | ib_buffer_pool |
+-----------------------------+----------------+
1 row in set (0.00 sec)
Configuring the Dump Percentage for Buffer Pool Pages

参数 innodb_buffer_pool_dump_pct 指定要转储的最近使用的缓冲池页的百分比,默认为 25。可以在运行时配置:

SET GLOBAL innodb_buffer_pool_dump_pct=40;

也可以在参数文件中配置:

[mysqld]
innodb_buffer_pool_dump_pct=40
Saving the Buffer Pool State at Shutdown and Restoring it at Startup

参数 innodb_buffer_pool_dump_at_shutdown 指定在关闭 MySQL 时保存缓冲池状态,默认为 ON。可以在运行时设置:

SET GLOBAL innodb_buffer_pool_dump_at_shutdown=ON;

也可以在启动时设置:

mysqld --innodb-buffer-pool-load-at-startup=ON;
Saving and Restoring the Buffer Pool State Online

在运行时保存缓冲池状态:

SET GLOBAL innodb_buffer_pool_dump_now=ON;

在运行时恢复缓冲池状态:

SET GLOBAL innodb_buffer_pool_load_now=ON;
Displaying Buffer Pool Dump Progress

在保存缓冲池状态到磁盘时查看进度:

SHOW STATUS LIKE 'Innodb_buffer_pool_dump_status';
Displaying Buffer Pool Load Progress

在加载缓冲池时查看进度:

SHOW STATUS LIKE 'Innodb_buffer_pool_load_status';
Aborting a Buffer Pool Load Operation

终止缓冲池加载:

SET GLOBAL innodb_buffer_pool_load_abort=ON;
Monitoring Buffer Pool Load Progress Using Performance Schema
  1. 启用 stage/innodb/buffer pool load

先查看:

[(none)]> SELECT NAME,ENABLED FROM performance_schema.setup_instruments WHERE NAME LIKE 'stage/innodb/buffer%';
+-------------------------------+---------+
| NAME                          | ENABLED |
+-------------------------------+---------+
| stage/innodb/buffer pool load | YES     |
+-------------------------------+---------+
1 row in set (0.01 sec)

如果 ENABLE 不为 YES,使用以下语句修改:

mysql> UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' 
       WHERE NAME LIKE 'stage/innodb/buffer%';
  1. 启用事件使用者表:
[(none)]> SELECT * FROM performance_schema.setup_consumers WHERE NAME LIKE '%stages%';
+----------------------------+---------+
| NAME                       | ENABLED |
+----------------------------+---------+
| events_stages_current      | NO      |
| events_stages_history      | NO      |
| events_stages_history_long | NO      |
+----------------------------+---------+
3 rows in set (0.00 sec)

[(none)]> UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME LIKE '%stages%';
Query OK, 3 rows affected (0.00 sec)
Rows matched: 3  Changed: 3  Warnings: 0

[(none)]> SELECT * FROM performance_schema.setup_consumers WHERE NAME LIKE '%stages%';
+----------------------------+---------+
| NAME                       | ENABLED |
+----------------------------+---------+
| events_stages_current      | YES     |
| events_stages_history      | YES     |
| events_stages_history_long | YES     |
+----------------------------+---------+
3 rows in set (0.00 sec)
  1. 启用参数 innodb_buffer_pool_dump_now 转储当前缓冲池状态:
[(none)]> SET GLOBAL innodb_buffer_pool_dump_now=ON;
Query OK, 0 rows affected (0.00 sec)
  1. 查看缓冲池转储状态,确认操作完成:
[(none)]> SHOW STATUS LIKE 'Innodb_buffer_pool_dump_status'\G
*************************** 1. row ***************************
Variable_name: Innodb_buffer_pool_dump_status
        Value: Buffer pool(s) dump completed at 230327 17:03:53
1 row in set (0.01 sec)
  1. 启用参数 innodb_buffer_pool_load_now 加载缓冲池:
[(none)]> SET GLOBAL innodb_buffer_pool_load_now=ON;
Query OK, 0 rows affected (0.00 sec)
  1. 查询表 PERFORMANCE_SCHEMA.EVENTS_STAGES_CURRENT,查看缓冲池加载状态,如果加载完成,返回空。
mysql> SELECT EVENT_NAME, WORK_COMPLETED, WORK_ESTIMATED
       FROM performance_schema.events_stages_current;
+-------------------------------+----------------+----------------+
| EVENT_NAME                    | WORK_COMPLETED | WORK_ESTIMATED |
+-------------------------------+----------------+----------------+
| stage/innodb/buffer pool load |           5353 |           7167 |
+-------------------------------+----------------+----------------+

WORK_COMPLETED 表示已加载的缓冲池页数,WORK_ESTIMATED 表示估计的剩余页数。

  1. 查询表 PERFORMANCE_SCHEMA.EVENTS_STAGES_HISTORY,查看完成的事件:
[(none)]> SELECT EVENT_NAME, WORK_COMPLETED, WORK_ESTIMATED FROM performance_schema.events_stages_history;
+-------------------------------+----------------+----------------+
| EVENT_NAME                    | WORK_COMPLETED | WORK_ESTIMATED |
+-------------------------------+----------------+----------------+
| stage/innodb/buffer pool load |            283 |            283 |
+-------------------------------+----------------+----------------+
1 row in set (0.01 sec)

Excluding Buffer Pool Pages from Core Files

Core 文件记录运行进程的状态和内存映像。所以当 mysqldopen in new window 进程失效时,具有大缓冲池的系统可以生成大 Core 文件。 要减小 Core 文件大小,可以禁用参数 innodb_buffer_pool_in_core_file,以从 Core 转储中忽略缓冲池页。此参数是在 MySQL 8.0.14 中引入的,默认启用。

[(none)]> SHOW VARIABLES LIKE 'innodb_buffer_pool_in_core_file';
+---------------------------------+-------+
| Variable_name                   | Value |
+---------------------------------+-------+
| innodb_buffer_pool_in_core_file | ON    |
+---------------------------------+-------+
1 row in set (0.01 sec)

只有在启用了参数 core_file,并且操作系统支持 madvise() 系统调用 MADV_DONTDUMP non-POSIX 扩展时(Linux 3.4 及以上),禁用 innodb_buffer_pool_in_core_file 才会生效。MADV_DONTDUMP 扩展从 Core 转储中排除指定范围的页面。

假设操作系统支持 MADV_DONTDUMP 扩展,使用 --core-file--innodb-buffer-pool-in-core-file=OFF 选项启动服务器,以生成没有缓冲池页的 Core 文件:

$> mysqld --core-file --innodb-buffer-pool-in-core-file=OFF

参数 core_file 只读,默认禁用,在启动的时候使用 --core-file 选项启用。

[(none)]> SHOW VARIABLES LIKE 'core_file';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| core_file     | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

参数 innodb_buffer_pool_in_core_file 可以动态修改:

mysql> SET GLOBAL innodb_buffer_pool_in_core_file=OFF;

Core 文件配置方案:

core_file variableinnodb_buffer_pool_in_core_file variablemadvise() MADV_DONTDUMP SupportOutcome
OFF (default)Not relevant to outcomeNot relevant to outcomeCore file is not generated
ONON (default)Not relevant to outcomeCore file is generated with buffer pool pages
ONOFFYesCore file is generated without buffer pool pages
ONOFFNoCore file is not generated, core_file is disabled, and a warning is written to the server error log

Core 文件大小还会受到 InnoDB 页大小的影响。较小的页大小意味着相同数量的数据需要更多的页,而更多的页意味着更多的页元数据。下表提供了不同页大小的 1G 缓冲池的示例:

innodb_page_size SettingBuffer Pool Pages Included (innodb_buffer_pool_in_core_file=ON)Buffer Pool Pages Excluded (innodb_buffer_pool_in_core_file=OFF)
4KB2.1GB0.9GB
64KB1.7GB0.7GB

Configuring Thread Concurrency for InnoDB

InnoDB 使用操作系统线程来处理用户请求,默认对并发线程的数量没有限制。

可以使用参数 innodb_thread_concurrency 指定并发线程的数量,当执行线程的数量达到这个限制时,新请求会在下次请求前休眠一段时间,这段时间由参数 innodb_thread_sleep_delay (默认为 10000 microseconds)控制,此参数由系统动态调整,但不能超过参数 innodb_adaptive_max_sleep_delay (默认为 150000 microseconds)指定的最大值。如果休眠后再请求还是没有多余线程提供其执行,那么就会进入到先进先出的队列中等待执行。等待的线程不计入并发线程数。

参数 innodb_thread_concurrency 默认值为 0,对并发线程的数量没有限制。如果数据库没有出现性能问题,使用此默认值即可。

[(none)]> SHOW VARIABLES LIKE 'innodb_thread_concurrency';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| innodb_thread_concurrency | 0     |
+---------------------------+-------+
1 row in set (0.01 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_thread_sleep_delay';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| innodb_thread_sleep_delay | 10000 |
+---------------------------+-------+
1 row in set (0.00 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_adaptive_max_sleep_delay';
+---------------------------------+--------+
| Variable_name                   | Value  |
+---------------------------------+--------+
| innodb_adaptive_max_sleep_delay | 150000 |
+---------------------------------+--------+
1 row in set (0.00 sec)

InnoDB 只有在并发线程数量有限的情况下才会导致线程休眠,当线程数量没有限制时,所有线程都会平等地竞争以获得调度。也就是说,如果参数 innodb_thread_concurrency 等于 0 时,忽略参数 innodb_thread_sleep_delay 的值。

当参数 innodb_thread_concurrency 大于 0 时,InnoDB 为允许执行的线程分配凭据,凭据数量由参数 innodb_concurrency_tickets 指定,默认为 5000。线程执行请求会消费凭据,凭据使用完成后,线程将会被驱逐,并有可能回到先进先出队列,等待再次运行并分配凭据。

[(none)]> SHOW VARIABLES LIKE 'innodb_concurrency_tickets';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| innodb_concurrency_tickets | 5000  |
+----------------------------+-------+
1 row in set (0.00 sec)

Configuring the Number of Background InnoDB I/O Threads

InnoDB 使用后台线程为各种类型的 I/O 请求提供服务。使用参数 innodb_read_io_threadsinnodb_write_io_threads 指定用于读取和写入请求的后台线程的数量,默认为 4,范围为 1 到 64,可以在参数文件中配置,不能动态修改。

[(none)]> SHOW VARIABLES LIKE 'innodb_read_io_threads';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_read_io_threads | 4     |
+------------------------+-------+
1 row in set (0.00 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_write_io_threads';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_write_io_threads | 4     |
+-------------------------+-------+
1 row in set (0.00 sec)

如果存储的 IOPS 很高,且在 SHOW ENGINE INNODB STATUS 输出中发现有超过 64 × innodb_read_io_threads 挂起的读取请求,则可以通过增加 innodb_read_io_threads 的值来提高性能。

Using Asynchronous I/O on Linux

InnoDB 使用 Linux 上的异步 I/O 子系统(原生 AIO)来执行数据文件页的预读和写入请求。此行为由参数 innodb_use_native_aio 控制,仅适用于 Linux 系统,默认启用,需要 libaio 库。在其他 Unix-like 系统,InnoDB 只使用同步 I/O。

[(none)]> SHOW VARIABLES LIKE 'innodb_use_native_aio';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_use_native_aio | ON    |
+-----------------------+-------+
1 row in set (0.01 sec)

使用原生 AIO,查询线程将 I/O 请求直接分发给操作系统,而不管参数 innodb_read_io_threadsinnodb_write_io_threads 的限制。

SHOW ENGINE INNODB STATUS 输出中显示许多挂起的读/写请求时,原生 AIO 就很适合这种 I/O 繁忙的系统。

Configuring InnoDB I/O Capacity

InnoDB 主线程和其他线程在后台执行各种任务,其中大多数与 I/O 相关,其试图以一种不会对服务器的正常工作产生不利影响的方式来执行这些任务。

使用参数 innodb_io_capacity 指定 InnoDB 可用的 I/O 总容量,默认为 200 IOPS。可以在参数文件中配置,也可以使用 SET GLOBAL 语句动态修改。该配置将平均分配给缓冲池实例。

[(none)]> SHOW VARIABLES LIKE 'innodb_io_capacity';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| innodb_io_capacity | 200   |
+--------------------+-------+
1 row in set (0.01 sec)
  • 对于 7200 RPM 的硬盘,建议配置为 100。
  • 低端 SSD,建议配置为 200。
  • 高端 SSD,建议配置为 1000。
  • 不建议高于 20000。

Ignoring I/O Capacity at Checkpoints

默认启用的参数 innodb_flush_sync 会在检查点导致大量 I/O 时忽略参数 innodb_io_capacity 的设置。

[(none)]> SHOW VARIABLES LIKE 'innodb_flush_sync';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| innodb_flush_sync | ON    |
+-------------------+-------+
1 row in set (0.01 sec)

如果要遵守 innodb_io_capacity 定义的 I/O 速率,则需要禁用 innodb_flush_sync,可以在参数文件中配置,也可以使用 SET GLOBAL 语句动态修改。

Configuring an I/O Capacity Maximum

使用参数 innodb_io_capacity_max 指定 IOPS 的最大值。

如果在启动的时候指定了 innodb_io_capacity 而没有指定 innodb_io_capacity_max,则 innodb_io_capacity_maxinnodb_io_capacity 的 2 倍 和 2000 这两者中较大的值。

[(none)]> SHOW VARIABLES LIKE 'innodb_io_capacity_max';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_io_capacity_max | 2000  |
+------------------------+-------+
1 row in set (0.01 sec)

如果要配置 innodb_io_capacity_max,建议设置为 innodb_io_capacity 的 两倍,不能低于 innodb_io_capacity

使用 SET GLOBAL innodb_io_capacity_max=DEFAULT 将其设置为最大值。

Configuring Spin Lock Polling

InnoDB 通过自旋锁(例如 mutexes 和 rw-locks)避免上下文切换。在多核系统上,多个线程一起自旋抢同一个锁容易造成 “cache ping-pong”,导致处理器使彼此的缓存部分无效。通过参数 innodb_spin_wait_delay 指定 PAUSE 指令的随机数量来缓解 “cache ping-pong”。也就是本来通过 CPU 高速自旋抢锁,换成了抢锁失败后随机延迟一下但是不释放 CPU,延迟时间到后继续抢锁,这样不但避免了上下文切换也大大减少了 “cache ping-pong”。

参数 innodb_spin_wait_delay 默认为 6。

[(none)]> SHOW VARIABLES LIKE 'innodb_spin_wait_delay';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_spin_wait_delay | 6     |
+------------------------+-------+
1 row in set (0.00 sec)

表示 PAUSE 指令的随机数量为:

{0,1,2,3,4,5}

则自旋等待时间为该随机数 * 50:

{0,50,100,150,200,250}

50 在 MySQL 8.0.16 之前是硬编码的,之后使用参数 innodb_spin_wait_pause_multiplier 进行配置。对于较长 PAUSE 指令的处理器可以减少该值。

[(none)]> SHOW VARIABLES LIKE 'innodb_spin_wait_pause_multiplier';
+-----------------------------------+-------+
| Variable_name                     | Value |
+-----------------------------------+-------+
| innodb_spin_wait_pause_multiplier | 50    |
+-----------------------------------+-------+
1 row in set (0.00 sec)

参数 innodb_spin_wait_delayinnodb_spin_wait_pause_multiplier 可以在参数文件中配置,也可以使用 SET GLOBAL 语句动态修改。在没有出现性能问题时,不建议修改。

Purge Configuration

当使用 SQL 语句删除某一行时,InnoDB 不会立即从数据库中物理删除该行。只有当 InnoDB 丢弃为删除而写的 UNDO 日志记录时,行及其索引记录才会被物理删除。这种删除操作仅在多版本并发控制(MVCC)或回滚不再需要该行之后才会发生,称为 Purge。

Configuring Purge Threads

Purge 操作由一个或多个 Purge 线程在后台执行。使用参数 innodb_purge_threads 指定允许 Purge 线程最大值,默认为 4,范围为 1 到 32。

[(none)]> SHOW VARIABLES LIKE 'innodb_purge_threads';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| innodb_purge_threads | 4     |
+----------------------+-------+
1 row in set (0.00 sec)

Configuring Purge Batch Size

使用参数 innodb_purge_batch_size 指定从历史列表中一批次处理 UNDO 日志页数,默认为 300,一般不需要进行调整。

[(none)]> SHOW VARIABLES LIKE 'innodb_purge_batch_size';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_purge_batch_size | 300   |
+-------------------------+-------+
1 row in set (0.00 sec)

Configuring the Maximum Purge Lag

使用参数 innodb_max_purge_lag 指定最大 Purge 延迟,当 Purge 延迟超过这个值时,将对 INSERT、UPDATE 和 DELETE 操作施加延迟,以便 Purge 操作有时间赶上。默认值为 0,表示没有最大 Purge 延迟。

[(none)]> SHOW VARIABLES LIKE 'innodb_max_purge_lag';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| innodb_max_purge_lag | 0     |
+----------------------+-------+
1 row in set (0.01 sec)

InnoDB 维护一个事务列表,包含由 UPDATE 或者 DELETE 操作标记为删除的索引记录,此列表的长度即为 Purge 延迟。在 MySQL 8.0.14 之前,Purge 延迟是通过以下公式计算的,这导致最小延迟为 5000 微秒:

(purge lag/innodb_max_purge_lag - 0.5) * 10000

从 MySQL 8.0.14 开始,Purge 延迟通过以下修订公式计算,将最小延迟减少到 5 微秒:

(purge_lag/innodb_max_purge_lag - 0.9995) * 10000

对于大负载,典型的 innodb_max_purge_lag 设置可能是 1000000,假设事务很小,大小只有 100B,对应的就是 100M 的未 Purge 的行。

SHOW ENGINE INNODB STATUS 输出的 TRANSACTIONS 部分,“History list length” 表示 Purge 延迟:

mysql> SHOW ENGINE INNODB STATUS;
...
------------
TRANSACTIONS
------------
Trx id counter 0 290328385
Purge done for trx's n:o < 0 290315608 undo n:o < 0 17
History list length 20

导致 “History list length” 增加的长事务有:

  • 当存在大量并发DML时,使用 --single-transaction 选项的 mysqldump 操作。
  • 在禁用自动提交后运行 SELECT 查询,忘记显式执行 COMMIT 或 ROLLBACK。

使用参数 innodb_max_purge_lag_delay 指定最大延迟,默认为 0,最大为 10000000 microseconds。

[(none)]> SHOW VARIABLES LIKE 'innodb_max_purge_lag_delay';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| innodb_max_purge_lag_delay | 0     |
+----------------------------+-------+
1 row in set (0.00 sec)

Configuring Optimizer Statistics for InnoDB

本节介绍如何为 InnoDB 表配置持久和非持久优化器统计信息。

持久优化器统计信息在服务器重新启动后保持不变,从而实现更高的执行计划稳定性和更一致的查询性能:

  • 可以使用参数 innodb_stats_auto_recalc 控制在表发生大的更改后是否自动更新统计信息。
  • 可以使用 CREATE TABLEALTER TABLESTATS_PERSISTENT, STATS_AUTO_RECALCSTATS_SAMPLE_PAGES 子句为表配置优化器统计信息。
  • 可以使用表 mysql.innodb_table_statsmysql.innodb_index_stats 查询优化器统计数据。
  • 可以查询表 mysql.innodb_table_statsmysql.innodb_index_statslast_update 字段查看统计信息上次更新的时间。
  • 可以修改表 mysql.innodb_table_statsmysql.innodb_index_stats ,以强制执行特定的查询优化计划或在不修改数据库的情况下测试替代计划。

持久优化器统计特性默认启用(innodb_stats_persistent=ON)。

非持久优化器统计信息将在每次服务器重新启动后以及其他一些操作后清除,并在下一次访问表时重新计算。因此,在重新计算统计数据时可能会产生不同的结果,从而导致不同的执行计划以及查询性能的变化。

Configuring Persistent Optimizer Statistics Parameters

持久优化器统计信息功能通过将统计信息存储到磁盘来提高执行计划的稳定性。

参数 innodb_stats_persistent 设置为 ON(默认为 ON),或者表设置为 STATS_PERSISTENT=1,优化器统计信息将持久化到磁盘。

[(none)]> SHOW VARIABLES LIKE 'innodb_stats_persistent';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_stats_persistent | ON    |
+-------------------------+-------+
1 row in set (0.00 sec)

持久统计信息存储在表 mysql.innodb_table_statsmysql.innodb_index_stats 中。

Configuring Automatic Statistics Calculation for Persistent Optimizer Statistics

使用参数 innodb_stats_auto_recalc (默认为 ON)指定当表的 10% 以上的行发生变更时是否自动计算统计信息。还可以在创建或更改表时指定 STATS_AUTO_RECALC 子句,为表配置自动计算统计信息。

[(none)]> SHOW VARIABLES LIKE 'innodb_stats_auto_recalc';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_stats_auto_recalc | ON    |
+--------------------------+-------+
1 row in set (0.00 sec)

由于自动计算统计信息的异步性质(后台),即使启用了 innodb_stats_auto_recalc,在运行影响表 10% 以上的 DML 操作后,也可能无法立即重新计算统计信息,可能会延迟几秒钟。如果需要立即更新统计信息,运行 ANALYZE TABLE 启动统计信息的同步(前台)重新计算。

如果禁用了 innodb_stats_auto_recalc,在对索引字段进行了大量更改后,执行 ANALYZE TABLE 来确保优化器统计信息的准确性。建议在加载数据到数据库后以及在数据库负载较低的时候,执行 ANALYZE TABLE

当增加索引,或增加或删除列时,无论是否启用 innodb_stats_auto_recalc,都会计算索引统计信息并将其添加到 innodb_index_stats 表中。

Configuring Optimizer Statistics Parameters for Individual Tables

要覆盖 innodb_stats_persistentinnodb_stats_auto_recalcinnodb_stats_persistent_sample_pages 这几个全局变量的设置,单独为某个表配置优化器统计信息参数,可以使用 CREATE TABLEALTER TABLESTATS_PERSISTENT, STATS_AUTO_RECALCSTATS_SAMPLE_PAGES 子句。

STATS_PERSISTENT:指定是否为 InnoDB 表启用持久统计信息。值为 DEFAULT 表示与参数 innodb_stats_persistent 一致,值为 1 表示启用表的持久统计信息,值为 0 表示禁用。为表启用持久统计信息后,在加载表数据后使用 ANALYZE TABLE 计算统计信息。

STATS_AUTO_RECALC :指定是否自动重新计算持久统计信息。值为 DEFAULT 表示与参数 innodb_stats_auto_recalc 一致,值为 1 表示当表的 10% 以上的行发生变更时自动重算统计信息,值为 0 表示不自动重算统计信息。

STATS_SAMPLE_PAGES:指定在通过 ANALYZE TABLE 操作为索引列计算基数和其他统计信息时要采样的索引页数。

例如:

CREATE TABLE `t1` (
`id` int(8) NOT NULL auto_increment,
`data` varchar(255),
`date` datetime,
PRIMARY KEY  (`id`),
INDEX `DATE_IX` (`date`)
) ENGINE=InnoDB,
  STATS_PERSISTENT=1,
  STATS_AUTO_RECALC=1,
  STATS_SAMPLE_PAGES=25;
Configuring the Number of Sampled Pages for InnoDB Optimizer Statistics

使用参数 innodb_stats_persistent_sample_pages 指定计算统计信息时采样页数,默认值为 20,仅在参数 innodb_stats_persistent 启用时应用。

[(none)]> SHOW VARIABLES LIKE 'innodb_stats_persistent_sample_pages';
+--------------------------------------+-------+
| Variable_name                        | Value |
+--------------------------------------+-------+
| innodb_stats_persistent_sample_pages | 20    |
+--------------------------------------+-------+
1 row in set (0.01 sec)

遇到以下问题时,考虑修改设置:

  • 统计数据不够准确导致优化器选择次优执行计划。可以运行 SELECT DISTINCT 获取实际值,与 mysql.innodb_index_stats 中的统计值进行对比,来检查统计信息的准确性。如果确定统计信息不够准确,则应增加 innodb_stats_persistent_sample_pages,直到统计信息足够准确为止。然而,过多地增加采样页数可能会导致 ANALYZE TABLE 运行缓慢。
  • ANALYZE TABLE 运行缓慢。在这种情况下,应减少采样页数。如果无法在准确的统计信息和 ANALYZE TABLE 执行时间之间实现平衡,考虑减少表中索引列的数量或限制分区的数量,以降低 ANALYZE TABLE 的复杂性。
Including Delete-marked Records in Persistent Statistics Calculations

默认情况下,InnoDB 在计算统计数据时读取未提交的数据。如果未提交事务从表中删除行,则在计算统计信息时会排除掉标记为删除的行,这对于使用 READ UNCOMMITTED 以外事务隔离级别的其他对该表进行操作的事务产生非最佳执行计划。为避免这种情况,可以启用参数 innodb_stats_include_delete_marked (默认为 OFF)以确保在计算持久优化器统计信息时包含标记为删除的行。

[(none)]> SHOW VARIABLES LIKE 'innodb_stats_include_delete_marked';
+------------------------------------+-------+
| Variable_name                      | Value |
+------------------------------------+-------+
| innodb_stats_include_delete_marked | OFF   |
+------------------------------------+-------+
1 row in set (0.01 sec)

当启用参数 innodb_stats_include_delete_marked 时,ANALYZE TABLE 会在重新计算统计信息时考虑标记为删除的行。

参数 innodb_stats_include_delete_marked 是一个影响所有 InnoDB 表的全局设置,只适用于持久优化器统计信息。

InnoDB Persistent Statistics Tables

持久统计信息表有 mysql.innodb_table_statsmysql.innodb_index_stats

innodb_table_stats 字段:

Column nameDescription
database_nameDatabase name
table_nameTable name, partition name, or subpartition name
last_updateA timestamp indicating the last time that InnoDB updated this row
n_rowsThe number of rows in the table
clustered_index_sizeThe size of the primary index, in pages
sum_of_other_index_sizesThe total size of other (non-primary) indexes, in pages

innodb_index_stats 字段:

Column nameDescription
database_nameDatabase name
table_nameTable name, partition name, or subpartition name
index_nameIndex name
last_updateA timestamp indicating the last time the row was updated
stat_nameThe name of the statistic, whose value is reported in the stat_value column
stat_valueThe value of the statistic that is named in stat_name column
sample_sizeThe number of pages sampled for the estimate provided in the stat_value column
stat_descriptionDescription of the statistic that is named in the stat_name column

mysql.innodb_table_statsmysql.innodb_index_statslast_update 字段为上次更新时间:

[(none)]> SELECT * FROM mysql.innodb_table_stats WHERE table_name='animals'\G
*************************** 1. row ***************************
           database_name: menagerie
              table_name: animals
             last_update: 2023-02-08 17:18:10
                  n_rows: 8
    clustered_index_size: 1
sum_of_other_index_sizes: 0
1 row in set (0.00 sec)
[(none)]> SELECT * FROM mysql.innodb_index_stats WHERE table_name='animals'\G
*************************** 1. row ***************************
   database_name: menagerie
      table_name: animals
      index_name: PRIMARY
     last_update: 2023-02-08 17:18:10
       stat_name: n_diff_pfx01
      stat_value: 8
     sample_size: 1
stat_description: id
*************************** 2. row ***************************
   database_name: menagerie
      table_name: animals
      index_name: PRIMARY
     last_update: 2023-02-08 17:18:10
       stat_name: n_leaf_pages
      stat_value: 1
     sample_size: NULL
stat_description: Number of leaf pages in the index
*************************** 3. row ***************************
   database_name: menagerie
      table_name: animals
      index_name: PRIMARY
     last_update: 2023-02-08 17:18:10
       stat_name: size
      stat_value: 1
     sample_size: NULL
stat_description: Number of pages in the index
3 rows in set (0.01 sec)

可以手动修改表 mysql.innodb_table_statsmysql.innodb_index_stats ,以强制执行特定的查询优化计划或在不修改数据库的情况下测试替代计划。如果手动更新统计信息,需使用 FLUSH TABLE tbl_name 语句加载更新的统计信息。

主从同步环境,不会同步表 mysql.innodb_table_statsmysql.innodb_index_stats,但是会同步 ANALYZE TABLE 语句,以在从库运行。

InnoDB Persistent Statistics Tables Example

mysql.innodb_table_stats 中每一行表示一个表的信息。

例如,表 t1 包含主键,二级索引,唯一索引:

CREATE TABLE t1 (
a INT, b INT, c INT, d INT, e INT, f INT,
PRIMARY KEY (a, b), KEY i1 (c, d), UNIQUE KEY i2uniq (e, f)
) ENGINE=INNODB;

插入 5 条记录后:

[menagerie]> insert into t1 values(1,1,10,11,100,101),(1,2,10,11,200,102),(1,3,10,11,100,103),(1,4,10,12,200,104),(1,5,10,12,100,105);
Query OK, 5 rows affected (0.01 sec)
Records: 5  Duplicates: 0  Warnings: 0

[menagerie]> select * from t1;
+---+---+------+------+------+------+
| a | b | c    | d    | e    | f    |
+---+---+------+------+------+------+
| 1 | 1 |   10 |   11 |  100 |  101 |
| 1 | 2 |   10 |   11 |  200 |  102 |
| 1 | 3 |   10 |   11 |  100 |  103 |
| 1 | 4 |   10 |   12 |  200 |  104 |
| 1 | 5 |   10 |   12 |  100 |  105 |
+---+---+------+------+------+------+
5 rows in set (0.00 sec)

运行 ANALYZE TABLE 立即更新统计信息:

[menagerie]> ANALYZE TABLE t1;
+--------------+---------+----------+----------+
| Table        | Op      | Msg_type | Msg_text |
+--------------+---------+----------+----------+
| menagerie.t1 | analyze | status   | OK       |
+--------------+---------+----------+----------+
1 row in set (0.01 sec)

查看表统计信息,包括更新时间,表行数,聚簇索引页数,其他索引页数:

[menagerie]> SELECT * FROM mysql.innodb_table_stats WHERE table_name='t1'\G
*************************** 1. row ***************************
           database_name: menagerie
              table_name: t1
             last_update: 2023-03-29 11:10:46
                  n_rows: 5
    clustered_index_size: 1
sum_of_other_index_sizes: 2
1 row in set (0.00 sec)

查看索引统计信息:

[menagerie]> SELECT index_name, stat_name, stat_value, stat_description FROM mysql.innodb_index_stats WHERE table_name='t1';
+------------+--------------+------------+-----------------------------------+
| index_name | stat_name    | stat_value | stat_description                  |
+------------+--------------+------------+-----------------------------------+
| PRIMARY    | n_diff_pfx01 |          1 | a                                 |
| PRIMARY    | n_diff_pfx02 |          5 | a,b                               |
| PRIMARY    | n_leaf_pages |          1 | Number of leaf pages in the index |
| PRIMARY    | size         |          1 | Number of pages in the index      |
| i1         | n_diff_pfx01 |          1 | c                                 |
| i1         | n_diff_pfx02 |          2 | c,d                               |
| i1         | n_diff_pfx03 |          2 | c,d,a                             |
| i1         | n_diff_pfx04 |          5 | c,d,a,b                           |
| i1         | n_leaf_pages |          1 | Number of leaf pages in the index |
| i1         | size         |          1 | Number of pages in the index      |
| i2uniq     | n_diff_pfx01 |          2 | e                                 |
| i2uniq     | n_diff_pfx02 |          5 | e,f                               |
| i2uniq     | n_leaf_pages |          1 | Number of leaf pages in the index |
| i2uniq     | size         |          1 | Number of pages in the index      |
+------------+--------------+------------+-----------------------------------+
14 rows in set (0.00 sec)

字段 stat_name 显示以下类型的统计信息:

  • size:索引的总页数。
  • n_leaf_pages:索引的叶子页数。
  • n_diff_pfxNN:当 stat_name=n_diff_pfx01 时,字段 stat_value 为索引第一列不同值的数量;当 stat_name=n_diff_pfx02 时,字段 stat_value 为索引前两列不同值的数量;以此类推。字段 stat_description 为索引列。

对于主键索引,n_diff_pfxNN 有 2 行:

  • index_name=PRIMARYstat_name=n_diff_pfx01stat_value 为 1,表示索引第一列(字段 a)只有 1 个不同的值。
  • index_name=PRIMARYstat_name=n_diff_pfx02stat_value 为 5,表示索引前两列(字段 a,b)有 5 个不同的值。

对于二级索引,n_diff_pfxNN 有 4 行,虽然此非唯一二级索引只包含 2 个字段(c,d),但 InnoDB 会将主键列(a,b)加在后面:

  • index_name=i1stat_name=n_diff_pfx01stat_value 为 1,表示索引第一列(字段 c)只有 1 个不同的值。
  • index_name=i1stat_name=n_diff_pfx02stat_value 为 2,表示索引前两列(字段 c,d)有 2 个不同的值。
  • index_name=i1stat_name=n_diff_pfx03stat_value 为 2,表示索引前三列(字段 c,d,a)有 2 个不同的值。
  • index_name=i1stat_name=n_diff_pfx04stat_value 为 5,表示索引前四列(字段 c,d,a,b)有 5 个不同的值。

对于唯一索引,n_diff_pfxNN 有 2 行:

  • index_name=i2uniqstat_name=n_diff_pfx01stat_value 为 2,表示索引第一列(字段 e)有 2 个不同的值。
  • index_name=i2uniqstat_name=n_diff_pfx02stat_value 为 5,表示索引前两列(字段 e,f)有 5 个不同的值。
Retrieving Index Size Using the innodb_index_stats Table

查询表 mysql.innodb_index_stats 获取表,分区,子分区的索引大小。


[menagerie]> SELECT SUM(stat_value) pages, index_name,
    ->       SUM(stat_value)*@@innodb_page_size size
    ->       FROM mysql.innodb_index_stats WHERE table_name='t1'
    ->       AND stat_name = 'size' GROUP BY index_name;
+-------+------------+-------+
| pages | index_name | size  |
+-------+------------+-------+
|     1 | PRIMARY    | 16384 |
|     1 | i1         | 16384 |
|     1 | i2uniq     | 16384 |
+-------+------------+-------+
3 rows in set (0.01 sec)

对于分区和子分区,修改对应的 WHERE 条件:

[menagerie]> SELECT SUM(stat_value) pages, index_name,
    ->       SUM(stat_value)*@@innodb_page_size size
    ->       FROM mysql.innodb_index_stats WHERE table_name like 't1#P%'
    ->       AND stat_name = 'size' GROUP BY index_name;
Empty set (0.00 sec)

Configuring Non-Persistent Optimizer Statistics Parameters

本节介绍如何配置非持久优化器统计信息。参数 innodb_stats_persistent 设置为 OFF,或者表设置为 STATS_PERSISTENT=0,优化器统计信息将不会持久化到磁盘,而是存储在内存中,也会通过某些操作和在某些条件下定期更新。

Optimizer Statistics Updates

非持久优化器统计信息在以下情况下更新:

  • 运行 ANALYZE TABLE
  • 运行 SHOW TABLE STATUSSHOW INDEX,或者在启用参数 innodb_stats_on_metadata(默认为 OFF)时查询表 information_schema.TABLESinformation_schema.STATISTICS
  • 启动 MySQL 客户端时默认会使用 --auto-rehash 选项,会打开所有 InnoDB 表,打开表的操作会重新计算统计信息。
  • 表被第一次打开。
  • 自上次更新统计数据以来,InnoDB 检测到表的 1/16 被修改。

需要注意的是,启用参数 innodb_stats_on_metadata 可能会降低有大量表或索引数据库的访问速度,降低查询 InnoDB 表的执行计划的稳定性。

[(none)]> SHOW VARIABLES LIKE 'innodb_stats_on_metadata';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_stats_on_metadata | OFF   |
+--------------------------+-------+
1 row in set (0.01 sec)

使用 SET 语句全局配置,仅适用于参数 innodb_stats_persistent 设置为 OFF时:

SET GLOBAL innodb_stats_on_metadata=ON
Configuring the Number of Sampled Pages

使用参数 innodb_stats_transient_sample_pages 指定采样页数,默认值为 8,仅在参数 innodb_stats_persistent 禁用时应用,可以在运行时全局配置:

[(none)]> SHOW VARIABLES LIKE 'innodb_stats_transient_sample_pages';
+-------------------------------------+-------+
| Variable_name                       | Value |
+-------------------------------------+-------+
| innodb_stats_transient_sample_pages | 8     |
+-------------------------------------+-------+
1 row in set (0.00 sec)

当调整此参数时,需要注意:

  • 设置为 1 或 2 这样的小的值可能会导致 Cardinality(不同值的数量) 的估计不准确。
  • 增大该值可能会需要更多的磁盘读。
  • 基于不同的估计值,优化器可能会选择不同的执行计划。

Estimating ANALYZE TABLE Complexity for InnoDB Tables

对 InnoDB 表运行 ANALYZE TABLE 的复杂性取决于:

  • 采样页数,由参数 innodb_stats_persistent_sample_pagesinnodb_stats_transient_sample_pages 指定。
  • 表中的索引字段数。
  • 分区数,如果表没有分区,那么分区数为 1。

则运行 ANALYZE TABLE 的复杂性可以近似计算为以上三者的乘积。越大执行时间越长。

Configuring the Merge Threshold for Index Pages

可以为索引页配置 MERGE_THRESHOLD 值。删除行或者通过 UPDATE 操作缩短行后,如果索引页的 “page-full” 百分比低于 MERGE_THRESHOLD 值,InnoDB 会尝试将索引页与相邻索引页合并。MERGE_THRESHOLD 默认为 50,范围内为 1 到 50。

当索引页的 “page-full” 百分比低于 50% 时,InnoDB 会尝试将索引页与相邻页合并。 如果两个页都接近 50%,则在页合并后不久可能会出现页拆分。 如果频繁发生此合并拆分行为,则可能会对性能产生负面影响。 为避免频繁的合并拆分,可以降低 MERGE_THRESHOLD 值,以便 InnoDB 以较低的 “page-full” 百分比进行页合并。

可以为表或单个索引指定索引页的 MERGE_THRESHOLD。 为单个索引指定的 MERGE_THRESHOLD 值优先于为表指定的 MERGE_THRESHOLD 值。 如果未指定,则 MERGE_THRESHOLD 值默认为 50。

Setting MERGE_THRESHOLD for a Table

使用 CREATE TABLE 语句的 COMMENT 子句为表设置 MERGE_THRESHOLD:

CREATE TABLE t1 (
   id INT,
  KEY id_index (id)
) COMMENT='MERGE_THRESHOLD=45';

使用 ALTER TABLE 语句的 COMMENT 子句为表设置 MERGE_THRESHOLD:

CREATE TABLE t1 (
   id INT,
  KEY id_index (id)
);

ALTER TABLE t1 COMMENT='MERGE_THRESHOLD=40';

Setting MERGE_THRESHOLD for Individual Indexes

CREATE TABLE 语句中为索引设置 MERGE_THRESHOLD:

CREATE TABLE t1 (
   id INT,
  KEY id_index (id) COMMENT 'MERGE_THRESHOLD=40'
);

使用 ALTER TABLE 语句为索引设置 MERGE_THRESHOLD:

CREATE TABLE t1 (
   id INT,
  KEY id_index (id)
);

ALTER TABLE t1 DROP KEY id_index;
ALTER TABLE t1 ADD KEY id_index (id) COMMENT 'MERGE_THRESHOLD=40';

使用 CREATE INDEX 语句为索引设置 MERGE_THRESHOLD:

CREATE TABLE t1 (id INT);
CREATE INDEX id_index ON t1 (id) COMMENT 'MERGE_THRESHOLD=40';

Querying the MERGE_THRESHOLD Value for an Index

查询表 INFORMATION_SCHEMA.INNODB_INDEXES 获取 MERGE_THRESHOLD:

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_INDEXES WHERE NAME='id_index' \G
*************************** 1. row ***************************
       INDEX_ID: 91
           NAME: id_index
       TABLE_ID: 68
           TYPE: 0
       N_FIELDS: 1
        PAGE_NO: 4
          SPACE: 57
MERGE_THRESHOLD: 40

如果使用 COMMENT 子句为表显示设置 MERGE_THRESHOLD,则可以使用 SHOW CREATE TABLE 查看 MERGE_THRESHOLD:

mysql> SHOW CREATE TABLE t2 \G
*************************** 1. row ***************************
       Table: t2
Create Table: CREATE TABLE `t2` (
  `id` int(11) DEFAULT NULL,
  KEY `id_index` (`id`) COMMENT 'MERGE_THRESHOLD=40'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

如果使用 COMMENT 子句为索引显示设置 MERGE_THRESHOLD,则可以使用 SHOW INDEX 查看 MERGE_THRESHOLD:

mysql> SHOW INDEX FROM t2 \G
*************************** 1. row ***************************
        Table: t2
   Non_unique: 1
     Key_name: id_index
 Seq_in_index: 1
  Column_name: id
    Collation: A
  Cardinality: 0
     Sub_part: NULL
       Packed: NULL
         Null: YES
   Index_type: BTREE
      Comment:
Index_comment: MERGE_THRESHOLD=40

Measuring the Effect of MERGE_THRESHOLD Settings

INFORMATION_SCHEMA.INNODB_METRICS 提供了两个计数器,用于衡量 MERGE_THRESHOLD 设置对索引页面合并的影响。

mysql> SELECT NAME, COMMENT FROM INFORMATION_SCHEMA.INNODB_METRICS
       WHERE NAME like '%index_page_merge%';
+-----------------------------+----------------------------------------+
| NAME                        | COMMENT                                |
+-----------------------------+----------------------------------------+
| index_page_merge_attempts   | Number of index page merge attempts    |
| index_page_merge_successful | Number of successful index page merges |
+-----------------------------+----------------------------------------+

MERGE_THRESHOLD 设置太小可能会由于过多的空页空间而导致数据文件过大。

Enabling Automatic Configuration for a Dedicated MySQL Server

如果启用参数 innodb_dedicated_server(默认为 OFF),InnoDB 自动配置以下参数:

  • innodb_buffer_pool_size
  • innodb_redo_log_capacity,在 MySQL 8.0.30 之前,配置 innodb_log_file_sizeinnodb_log_files_in_group
  • innodb_flush_method

只有当 MySQL 实例位于可以使用所有可用系统资源的专用服务器上时,才考虑启用 innodb_dedicated_server。例如,如果在 Docker 容器或专用 VM 中运行 MySQL server,可以考虑启用 innodb_dedicated_server。如果 MySQL 实例与其他应用程序共享系统资源,则不建议启用 innodb_dedicated_server

各个参数自动配置如下:

  • innodb_buffer_pool_size:根据服务器内存进行配置。
Detected Server MemoryBuffer Pool Size
Less than 1GB128MB (the default value)
1GB to 4GBdetected server memory * 0.5
Greater than 4GBdetected server memory * 0.75
  • innodb_redo_log_capacity:根据服务器内存进行配置。
Detected Server MemoryBuffer Pool SizeRedo Log Capacity
Less than 1GBNot configured100MB
Less than 1GBLess than 1GB100MB
1GB to 2GBNot applicable100MB
2GB to 4GBNot configured1GB
2GB to 4GBAny configured valueround(0.5 * detected server memory in GB) * 0.5 GB
4GB to 10.66GBNot applicableround(0.75 * detected server memory in GB) * 0.5 GB
10.66GB to 170.66GBNot applicableround(0.5625 * detected server memory in GB) * 0.5 GB
Greater than 170.66GBNot applicable128GB
  • innodb_log_file_size :根据缓冲池大小配置。
Buffer Pool SizeLog File Size
Less than 8GB512MB
8GB to 128GB1024MB
Greater than 128GB2048MB
  • innodb_log_files_in_group:根据缓冲池大小配置,最小值为 2,自动配置从 MySQL 8.0.14 加入。
Buffer Pool SizeNumber of Log Files
Less than 8GBround(buffer pool size)
8GB to 128GBround(buffer pool size * 0.75)
Greater than 128GB64
  • innodb_flush_method:当启用参数 innodb_dedicated_server,刷新方式为 O_DIRECT_NO_FSYNC,如果 O_DIRECT_NO_FSYNC 不可用,innodb_flush_method 使用默认值(fsync)。O_DIRECT_NO_FSYNC 表示 InnoDB 在刷新 I/O 期间使用 O_DIRECT,但在每次写入操作后跳过 fsync() 系统调用。

警告:

在 MySQL 8.0.14 之前,此设置不适用于 XFS 和 EXT4 等文件系统,因为它们需要 fsync() 同步文件系统元数据更改。

从 MySQL 8.0.14 开始,在创建新文件、增加文件大小和关闭文件后调用 fsync() ,以确保文件系统元数据更改同步。在每次写入操作之后,仍会跳过 fsync() 系统调用。

如果重做日志文件和数据文件位于不同的存储设备上,并且在从没有后备电池的设备缓存中刷新数据文件写入之前发生意外退出,则可能会丢失数据。如果计划使用不同的存储设备来存储重做日志文件和数据文件,并且数据文件位于不带后备电池的缓存设备,请改用 O_DIRECT

如果在参数文件或其他地方手动配置了自动配置的参数,则使用手动指定的设置,会有类似如下启动警告信息:

[Warning] [000000] InnoDB: Option innodb_dedicated_server is ignored for innodb_buffer_pool_size because innodb_buffer_pool_size=134217728 is specified explicitly.

一个参数的手动配置不会影响其他参数的自动配置。如果启用参数 innodb_dedicated_server,手动配置了 innodb_buffer_pool_size,其他基于缓冲池大小配置的参数不使用手动配置的值,而是使用根据服务器内存计算的缓冲池大小值。

InnoDB Table and Page Compression

本节介绍 InnoDB 表压缩和 InnoDB 页压缩。

InnoDB Table Compression

Creating Compressed Tables

可以在独立表空间或者常规表空间中创建压缩表,不能在系统表空间中创建。

Creating a Compressed Table in File-Per-Table Tablespace

需要启用参数 innodb_file_per_table(默认启用),在 CREATE TABLEALTER TABLE 语句指定 ROW_FORMAT=COMPRESSED 或者 KEY_BLOCK_SIZE 子句(或者同时指定),在独立表空间中创建压缩表。

例如:

SET GLOBAL innodb_file_per_table=1;
CREATE TABLE t1
 (c1 INT PRIMARY KEY)
 ROW_FORMAT=COMPRESSED
 KEY_BLOCK_SIZE=8;
Restrictions on Compressed Tables
  • 压缩表不能存储在 InnoDB 系统表空间中。
  • 压缩应用于整个表及其所有相关索引,而不是单个行。
  • InnoDB 不支持压缩临时表。

Tuning Compression for InnoDB Tables

通常,对于读多写少,包含较多字符类型字段的表,压缩效果最好。

对表进行压缩测试,复制源表,进行压缩,对比文件大小。示例如下:

USE test;
SET GLOBAL innodb_file_per_table=1;
SET GLOBAL autocommit=0;

-- Create an uncompressed table with a million or two rows.
CREATE TABLE big_table AS SELECT * FROM information_schema.columns;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
INSERT INTO big_table SELECT * FROM big_table;
COMMIT;
ALTER TABLE big_table ADD id int unsigned NOT NULL PRIMARY KEY auto_increment;

SHOW CREATE TABLE big_table\G

select count(id) from big_table;

-- Check how much space is needed for the uncompressed table.
\! ls -l data/test/big_table.ibd

CREATE TABLE key_block_size_4 LIKE big_table;
ALTER TABLE key_block_size_4 key_block_size=4 row_format=compressed;

INSERT INTO key_block_size_4 SELECT * FROM big_table;
commit;

-- Check how much space is needed for a compressed table
-- with particular compression settings.
\! ls -l data/test/key_block_size_4.ibd
-rw-rw----  1 cirrus  staff  310378496 Jan  9 13:44 data/test/big_table.ibd
-rw-rw----  1 cirrus  staff  83886080 Jan  9 15:10 data/test/key_block_size_4.ibd

应用程序的总体性能、CPU 和 I/O 利用率以及磁盘文件的大小都很好地指示了压缩对应用程序的有效性。要查看压缩对特定工作负载是否有效,请执行以下操作:

  • 对于简单的测试,使用不带其他压缩表的 MySQL 实例,查询表 INFORMATION_SCHEMA.INNODB_CMP 获取每种压缩页的压缩活动信息,为数据库中所有压缩表的压缩统计信息。
  • 对于涉及多个压缩表的工作负载的更详细的测试,查询表 INFORMATION_SCHEMA.INNODB_CMP_PER_INDEX。由于收集表 INFORMATION_SCHEMA.INNODB_CMP_PER_INDEX 的统计信息成本很高,因此在查询该表之前,必须启用参数 innodb_cmp_per_index_enabled(默认为 OFF)。最好在测试环境进行此类测试。
  • 对正在测试的压缩表运行一些典型的 SQL 语句。
  • 通过查询 INFORMATION_SCHEMA.INNODB_CMPINFORMATION_SCHEMA.INNODB_CMP_PER_INDEX,对比 COMPRESS_OPSCOMPRESS_OPS_OK,检查整体压缩操作中成功的比率。
  • 如果成功比例较高,那么该表可能适合压缩。
  • 如果失败比例较高,可以调整参数 innodb_compression_levelinnodb_compression_failure_threshold_pctinnodb_compression_pad_pct_max,然后重试。
Choosing the Compressed Page Size

压缩页大小的最佳设置取决于表及其索引所包含的数据类型和分布。压缩页大小应始终大于最大记录大小,否则操作可能会失败。

将压缩页设置得太大会浪费一些空间,但不必经常压缩页。如果压缩页大小设置得太小,则插入或更新可能需要耗时的重新压缩,并且B树节点可能更频繁地拆分,从而导致数据文件更大,索引效率更低。

通常,将压缩页大小设置为 8K 或 4K 字节。假设 InnoDB 表的最大行大小约为 8K,则 KEY_BLOCK_SIZE=8 为合适的设置。

Compression for OLTP Workloads

InnoDB 推荐在只读或以读取为主的工作负载场景使用压缩功能,例如数据仓库。

对于写密集型的 OLTP,相关压缩参数:

innodb_compression_level:指定压缩级别,默认为 6,范围为 0 到 9。值越大,压缩率越高,CPU 开销越大。

innodb_compression_failure_threshold_pct:指定在更新压缩表期间压缩失败的百分比阈值,默认为 5。当超过此阈值时,MySQL 开始在每个新的压缩页中留下额外的可用空间,并动态调整可用空间,使其达到参数 innodb_compression_pad_pct_max 指定的页大小百分比。

innodb_compression_pad_pct_max:指定页中保留的最大空间,用于记录对压缩行的更改,而无需再次压缩整个页,默认为 50。

innodb_log_compressed_pages:指定是否将重新压缩的页映像写入重做日志。默认启用,以防止在恢复过程中使用不同版本的 zlib 压缩算法可能发生的损坏。如果确定 zlib 版本不会更改,可以禁用 innodb_log_compressed_pages,以减少修改压缩数据的重做日志生成。

[(none)]> SHOW VARIABLES LIKE 'innodb_compression_level';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_compression_level | 6     |
+--------------------------+-------+
1 row in set (0.14 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_compression_failure_threshold_pct';
+------------------------------------------+-------+
| Variable_name                            | Value |
+------------------------------------------+-------+
| innodb_compression_failure_threshold_pct | 5     |
+------------------------------------------+-------+
1 row in set (0.02 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_compression_pad_pct_max';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_compression_pad_pct_max | 50    |
+--------------------------------+-------+
1 row in set (0.01 sec)

[(none)]> SHOW VARIABLES LIKE 'innodb_log_compressed_pages';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| innodb_log_compressed_pages | ON    |
+-----------------------------+-------+
1 row in set (0.01 sec)

在使用压缩数据时,有时需要将页的压缩版本和未压缩版本同时保存在内存中,因此在 OLTP 中使用压缩时,建议增加 innodb_buffer_pool_size

SQL Compression Syntax Warnings and Errors

SQL Compression Syntax Warnings and Errors for File-Per-Table Tablespaces

当启用参数 innodb_strict_mode(默认为 ON),禁用参数 innodb_file_per_table,在 CREATE TABLE 或者 ALTER TABLE 语句中指定 ROW_FORMAT=COMPRESSED 或者 KEY_BLOCK_SIZE 报错如下:

ERROR 1031 (HY000): Table storage engine for 't1' doesn't have this option

表示创建压缩表失败。

当禁用参数 innodb_strict_mode(默认为 ON),禁用参数 innodb_file_per_table,在 CREATE TABLE 或者 ALTER TABLE 语句中指定 ROW_FORMAT=COMPRESSED 或者 KEY_BLOCK_SIZE 告警如下:

mysql> SHOW WARNINGS;
+---------+------+---------------------------------------------------------------+
| Level   | Code | Message                                                       |
+---------+------+---------------------------------------------------------------+
| Warning | 1478 | InnoDB: KEY_BLOCK_SIZE requires innodb_file_per_table.        |
| Warning | 1478 | InnoDB: ignoring KEY_BLOCK_SIZE=4.                            |
| Warning | 1478 | InnoDB: ROW_FORMAT=COMPRESSED requires innodb_file_per_table. |
| Warning | 1478 | InnoDB: assuming ROW_FORMAT=DYNAMIC.                          |
+---------+------+---------------------------------------------------------------+

表示创建了一个非压缩表。

只有在省略 ROW_FORMAT 或者指定其为 COMPRESSED 时,才能使用 KEY_BLOCK_SIZE 。当禁用参数 innodb_strict_mode ,指定 KEY_BLOCK_SIZE 并使用其他 ROW_FORMAT 时,告警如下:

LevelCodeMessage
Warning1478InnoDB: ignoring KEY_BLOCK_SIZE=n unless ROW_FORMAT=COMPRESSED.

当启用参数 innodb_strict_mode ,指定 KEY_BLOCK_SIZE 并使用其他 ROW_FORMAT 时,会报错,不会创建表。

CREATE TABLE 或者 ALTER TABLEROW_FORMATKEY_BLOCK_SIZE 选项如下:

OptionUsage NotesDescription
ROW_FORMAT=REDUNDANTStorage format used prior to MySQL 5.0.3Less efficient than ROW_FORMAT=COMPACT; for backward compatibility
ROW_FORMAT=COMPACTDefault storage format since MySQL 5.0.3Stores a prefix of 768 bytes of long column values in the clustered index page, with the remaining bytes stored in an overflow page
ROW_FORMAT=DYNAMICStore values within the clustered index page if they fit; if not, stores only a 20-byte pointer to an overflow page (no prefix)
ROW_FORMAT=COMPRESSEDCompresses the table and indexes using zlib
KEY_BLOCK_SIZE=nSpecifies compressed page size of 1, 2, 4, 8 or 16 kilobytes; implies ROW_FORMAT=COMPRESSED. For general tablespaces, a KEY_BLOCK_SIZE value equal to the InnoDB page size is not permitted.

当禁用参数 innodb_strict_modeCREATE TABLE 或者 ALTER TABLE 的告警和错误:

SyntaxWarning or Error ConditionResulting ROW_FORMAT, as shown in SHOW TABLE STATUS
ROW_FORMAT=REDUNDANTNoneREDUNDANT
ROW_FORMAT=COMPACTNoneCOMPACT
ROW_FORMAT=COMPRESSED or ROW_FORMAT=DYNAMIC or KEY_BLOCK_SIZE is specifiedIgnored for file-per-table tablespaces unless innodb_file_per_tableopen in new window is enabled.the default row format for file-per-table tablespaces; the specified row format for general tablespaces
Invalid KEY_BLOCK_SIZE is specified (not 1, 2, 4, 8 or 16)KEY_BLOCK_SIZE is ignoredthe specified row format, or the default row format
ROW_FORMAT=COMPRESSED and valid KEY_BLOCK_SIZE are specifiedNone; KEY_BLOCK_SIZE specified is usedCOMPRESSED
KEY_BLOCK_SIZE is specified with REDUNDANT, COMPACT or DYNAMIC row formatKEY_BLOCK_SIZE is ignoredREDUNDANT, COMPACT or DYNAMIC
ROW_FORMAT is not one of REDUNDANT, COMPACT, DYNAMIC or COMPRESSEDIgnored if recognized by the MySQL parser. Otherwise, an error is issued.the default row format or N/A

InnoDB Page Compression

InnoDB 支持对独立表空间中的表进行页级别压缩,被称之为透明页面压缩。通过使用 CREATE TABLE 或者 ALTER TABLECOMPRESSION 属性启用页压缩。支持的压缩算法包括 ZlibLZ4

Supported Platforms

  • Windows with NTFS

  • RHEL 7 and derived distributions that use kernel version 3.10.0-123 or higher

  • OEL 5.10 (UEK2) kernel version 2.6.39 or higher

  • OEL 6.5 (UEK3) kernel version 3.8.13 or higher

  • OEL 7.0 kernel version 3.8.13 or higher

  • SLE11 kernel version 3.0-x

  • SLE12 kernel version 3.12-x

  • OES11 kernel version 3.0-x

  • Ubuntu 14.0.4 LTS kernel version 3.13 or higher

  • Ubuntu 12.0.4 LTS kernel version 3.2 or higher

  • Debian 7 kernel version 3.2 or higher

Enabling Page Compression

CREATE TABLE 指定 COMPRESSION 属性启用页压缩:

CREATE TABLE t1 (c1 INT) COMPRESSION="zlib";

ALTER TABLE 指定 COMPRESSION 属性修改页压缩,使用 OPTIMIZE TABLE 对现有页面生效:

ALTER TABLE t1 COMPRESSION="zlib";
OPTIMIZE TABLE t1;

Disabling Page Compression

ALTER TABLE 指定 COMPRESSION=None 属性禁用页压缩,使用 OPTIMIZE TABLE 解压现有页面:

ALTER TABLE t1 COMPRESSION="None";
OPTIMIZE TABLE t1;

Page Compression Metadata

查询表 INFORMATION_SCHEMA.INNODB_TABLESPACES 获取页压缩信息。

# Create the employees table with Zlib page compression

CREATE TABLE employees (
    emp_no      INT             NOT NULL,
    birth_date  DATE            NOT NULL,
    first_name  VARCHAR(14)     NOT NULL,
    last_name   VARCHAR(16)     NOT NULL,
    gender      ENUM ('M','F')  NOT NULL,
    hire_date   DATE            NOT NULL,
    PRIMARY KEY (emp_no)
) COMPRESSION="zlib";

# Insert data (not shown)

# Query page compression metadata in INFORMATION_SCHEMA.INNODB_TABLESPACES

mysql> SELECT SPACE, NAME, FS_BLOCK_SIZE, FILE_SIZE, ALLOCATED_SIZE FROM
       INFORMATION_SCHEMA.INNODB_TABLESPACES WHERE NAME='employees/employees'\G
*************************** 1. row ***************************
SPACE: 45
NAME: employees/employees
FS_BLOCK_SIZE: 4096
FILE_SIZE: 23068672
ALLOCATED_SIZE: 19415040
  • FS_BLOCK_SIZE:文件系统块大小,这里为 4K。
  • FILE_SIZE:未压缩文件大小。
  • ALLOCATED_SIZE:页压缩后,实际文件大小。

Identifying Tables Using Page Compression

查询表 INFORMATION_SCHEMA.TABLES 获取启用页压缩的表。

mysql> SELECT TABLE_NAME, TABLE_SCHEMA, CREATE_OPTIONS FROM INFORMATION_SCHEMA.TABLES 
       WHERE CREATE_OPTIONS LIKE '%COMPRESSION=%';
+------------+--------------+--------------------+
| TABLE_NAME | TABLE_SCHEMA | CREATE_OPTIONS     |
+------------+--------------+--------------------+
| employees  | test         | COMPRESSION="zlib" |
+------------+--------------+--------------------+

也可以使用 SHOW CREATE TABLE 查看。

Page Compression Limitations and Usage Notes

  • 如果文件系统块大小 * 2 > innodb_page_size,则禁用页压缩。
  • 页压缩不支持共享表空间中的表,包括系统表空间、临时表空间和常规表空间。
  • 页压缩不支持 UNDO 日志表空间。
  • 页压缩不支持 REDO 日志页。
  • 页压缩不支持空间索引页。
  • 页压缩不支持压缩表。

InnoDB Row Formats

表的行格式决定了行的物理存储方式,这反过来又会影响查询和 DML 操作的性能。在单页中放入更多的行,则查询更快,缓冲池占用更少,写 I/O 也更少。

每个 InnoDB 表都有一个称为聚簇索引的特殊索引,用于存储行数据。通常,聚簇索引即是主键。聚簇索引以外的索引称为二级索引。在 InnoDB 中,二级索引中的每条记录都包含主键列,以及为二级索引指定的列。InnoDB 使用二级索引的主键值在聚簇索引中搜索对应的记录。

变长字段由于太长而无法放入到一个 B-tree 索引页,会被存放到单独分配的磁盘页,称为溢出页(Overflow Pages),这些字段被称为页外(Off-Page)字段。根据字段长度,将字段的全部值或者前缀值存放到 B-tree,以避免浪费存储空间和读取不必要的页。

InnoDB 支持四种行格式:

Row FormatCompact Storage CharacteristicsEnhanced Variable-Length Column StorageLarge Index Key Prefix SupportCompression SupportSupported Tablespace Types
REDUNDANTNoNoNoNosystem, file-per-table, general
COMPACTYesNoNoNosystem, file-per-table, general
DYNAMICYesYesYesNosystem, file-per-table, general
COMPRESSEDYesYesYesYesfile-per-table, general

REDUNDANT Row Format

REDUNDANT 行格式提供了与旧版本 MySQL 的兼容性。

使用 REDUNDANT 行格式的表将可变长度列值(VARCHAR、VARBINARY、BLOB 和 TEXT 类型)的前 768 个字节存储在 B-tree 索引中,其余部分存储在溢出页(Overflow Pages)。大于或等于 768 字节的固定长度列被认定为可变长度列,可以页外(Off-Page)存储。例如,如果字符集(例如 utf8mb4)的最大字节长度大于 3,则 CHAR(255) 列可能超过 768 个字节。

如果列值为 768 字节或更少,则不使用溢出页(Overflow Pages),该值完全存储在 B-tree 中,可能会节省一些 I/O。有许多 BLOB 列的表可能会导致 B-tree 节点变得太满,包含的行太少,从而使整个索引的效率低于行更短或列值存储在页外(Off-Page)的情况。

REDUNDANT 行格式具有以下存储特性:

  • 每个索引记录都包含一个 6 字节的标头。标头用于将连续记录链接在一起,并用于行级锁。
  • 聚簇索引中的记录包含用户定义的所有字段。此外,还有一个 6 字节的事务 ID 字段和一个 7 字节的回滚指针字段。
  • 如果没有为表定义主键,则每个聚簇索引记录还包含一个 6 字节的行 ID 字段。
  • 每个二级索引记录包含所有主键字段。
  • 记录包含指向该记录每个字段的指针。如果记录中字段的总长度小于 128 字节,则指针为 1 字节,否则为 2 字节。指针数组称为记录目录。指针指向的区域是记录的数据部分。
  • 在内部,固定长度字符字段(如 CHAR(10))以固定长度格式存储。VARCHAR 列中的尾部空格不会被截断。
  • 大于或等于 768 字节的固定长度列被认定为可变长度列,可以页外(Off-Page)存储。例如,如果字符集(例如 utf8mb4)的最大字节长度大于 3,则 CHAR(255) 列可能超过 768 字节。
  • SQL NULL 值在记录目录中保留一个或两个字节。如果存储在可变长度列中,SQL NULL 值将在记录的数据部分保留零个字节。对于固定长度的列,在记录的数据部分中保留该列的固定长度。为 NULL 值保留固定空间可以将列从 NULL 值就地更新为非 NULL 值,而不会导致索引页碎片。

COMPACT Row Format

与 REDUNDANT 行格式相比,COMPACT 行格式减少了约 20% 的行存储空间,代价是增加了某些操作的 CPU 使用量。如果工作负载受限于缓存命中率和磁盘速度,那么 COMPACT 格式可能会更快。如果工作负载受限于 CPU 速度,那么 COMPACT 格式可能会更慢。

使用 COMPACT 行格式的表将可变长度列值(VARCHAR、VARBINARY、BLOB 和 TEXT 类型)的前 768 个字节存储在 B-tree 索引中,其余部分存储在溢出页(Overflow Pages)。大于或等于 768 字节的固定长度列被认定为可变长度列,可以页外(Off-Page)存储。例如,如果字符集(例如 utf8mb4)的最大字节长度大于 3,则 CHAR(255) 列可能超过 768 个字节。

如果列值为 768 字节或更少,则不使用溢出页(Overflow Pages),该值完全存储在 B-tree 中,可能会节省一些 I/O。有许多 BLOB 列的表可能会导致 B-tree 节点变得太满,包含的行太少,从而使整个索引的效率低于行更短或列值存储在页外(Off-Page)的情况。

COMPACT 行格式具有以下存储特性:

  • 每个索引记录都包含一个 5 字节的标头,其前面可能有一个可变长度的标头部分。标头用于将连续记录链接在一起,并用于行级锁。

  • 记录头的可变长度部分包含一个用于指示 NULL 列的位向量。如果索引中可以为 NULL 的列数为 N,则位向量占用 CEILING(N/8) 字节(例如,如果有 9 到 16 列中的任意列可以为 NULL,则位向量使用 2 字节)。为 NULL 的列不会占用该向量以外的空间。记录头的可变长度部分还包含可变长度列的长度。每个长度占用 1 或 2 字节,具体取决于列的最大长度。如果索引中的所有列都不是 NULL 并且具有固定长度,则记录头没有可变长度部分。

  • 对于每个非 NULL 可变长度字段,记录头包含 1 或 2 字节的列长度。只有当列的一部分存储在 “Overflow Pages”,或者最大长度超过 255 字节,而实际长度超过 127 字节时,才需要 2 字节。对于外部存储的列,2 字节长度表示内部存储部分的长度加上指向外部存储部分的 20 字节指针。内部部分是 768 字节,所以长度是 768+20。20 字节的指针存储列的真实长度。

  • 记录头后面跟着非 NULL 列的数据内容。

  • 聚簇索引中的记录包含用户定义的所有字段。此外,还有一个 6 字节的事务 ID 字段和一个 7 字节的回滚指针字段。

  • 如果没有为表定义主键,则每个聚簇索引记录还包含一个 6 字节的行 ID 字段。

  • 每个二级索引记录包含所有主键字段。如果任何一个主键字段是可变长度的,则每个二级索引的记录头都有一个可变长度部分来记录它们的长度,即使二级索引是在固定长度字段上定义的。

  • 在内部,固定长度字符字段(如 CHAR(10))以固定长度格式存储。VARCHAR 列中的尾部空格不会被截断。

  • 在内部,对于 utf8mb3 和 utf8mb4 等可变长度字符集,InnoDB 试图通过修剪尾部空格将 CHAR(N) 存储在 N 字节中。如果 CHAR(N) 列值的长度超过 N 字节,则尾部空格将修剪为列值字节长度的最小值。CHAR(N) 列的最大长度是最大字符字节长度 × N。 为 CHAR(N) 预留了至少 N 个字节。在许多情况下,保留最小空间 N 可以在不造成索引页碎片的情况下就地执行列更新。相比之下,当使用 REDUNDANT 行格式时,CHAR(N) 列占据了最大字符字节长度 × N。 大于或等于 768 字节的固定长度列被认定为可变长度字段,可以页外(Off-Page)存储。例如,如果字符集(例如 utf8mb4)的最大字节长度大于 3,则 CHAR(255) 列可能超过 768 字节。

DYNAMIC Row Format

DYNAMIC 行格式提供了与 COMPACT 行格式相同的存储特性,但为较长的可变长度列添加了增强的存储功能,并支持大索引键前缀。

当用 ROW_FORMAT=DYNAMIC 创建表时,InnoDB可以完全在页外(Off-Page)存储较长的可变长度列值(VARCHAR、VARBINARY、BLOB 和 TEXT 类型),聚簇索引记录只包含一个指向溢出页(Overflow Pages)的 20 字节指针。大于或等于 768 字节的固定长度字段被认定为可变长度字段。例如,如果字符集(例如 utf8mb4)的最大字节长度大于 3,则 CHAR(255) 列可能超过 768 字节。

列是否页外(Off-Page)存储取决于页面大小和行的总长度。当一行太长时,会选择最长的列进行页外(Off-Page)存储,直到聚簇索引记录适合放入 B-tree 页。小于或等于 40 字节的 TEXT 和 BLOB 列存储在行中,不页外(Off-Page)存储。

如果适合的话,DYNAMIC 行格式将整行存储在索引节点中(COMPACT 和 REDUNDANT 格式也是如此),但DYNAMIC 行格式避免了用大量较长列数据字节填充 B-tree 节点的问题。DYNAMIC 行格式基于这样一种思想,即如果较长数据值的一部分存储在页外(Off-Page),那么通常最有效的方法就是将整个值存储在页外(Off-Page)。使用 DYNAMIC 行格式时,B-tree 节点中可能会保留较短的列,从而最大限度地减少给定行所需的溢出页(Overflow Pages)数。

DYNAMIC 行格式支持最多 3072 字节的索引键前缀。

使用 DYNAMIC 行格式的表可以存储在系统表空间、独立表空间和常规表空间中。要在系统表空间中存储 DYNAMIC 表,要么禁用 innodb_file_per_table 并使用 CREATE TABLEALTER TABLE 语句,或者在 CREATE TABLEALTER TABLE 语句中使用 TABLESPACE [=] innodb_system 选项。

DYNAMIC 行格式是 COMPACT 行格式的变体。存储特性参考:COMPACT Row Formatopen in new window

COMPRESSED Row Format

COMPRESSED 行格式提供了与 DYNAMIC 行格式相同的存储特性和功能,但增加了对表和索引数据压缩的支持。

COMPRESSED 行格式使用与 DYNAMIC 行格式类似的页外(Off-Page)存储内部细节,压缩表和索引数据,并使用较小的页尺寸,从而带来额外的存储节约和性能提升。对于 COMPRESSED 行格式,KEY_BLOCK_SIZE 选项控制在聚簇索引中存储多少列数据,以及在溢出页(Overflow Pages)上存放多少列数据。

COMPRESSED 行格式支持最多 3072 字节的索引键前缀。

使用 COMPRESSED 行格式的表可以在独立表空间或常规表空间中创建。系统表空间不支持 COMPRESSED 行格式。要将 COMPRESSED 表存储在独立表空间中,必须启用 innodb_file_per_table

COMPRESSED 行格式是 COMPACT 行格式的变体。存储特性参考:COMPACT Row Formatopen in new window

Defining the Row Format of a Table

使用参数 innodb_default_row_format 指定 InnoDB 表的默认行格式,默认为 DYNAMIC,可动态设置,不能指定为 COMPRESSED。当未明确指定 ROW_FORMAT 选项或者使用 ROW_FORMAT=DEFAULT 选项,则使用默认行格式。

[(none)]> SHOW VARIABLES LIKE 'innodb_default_row_format';
+---------------------------+---------+
| Variable_name             | Value   |
+---------------------------+---------+
| innodb_default_row_format | dynamic |
+---------------------------+---------+
1 row in set (0.01 sec)

mysql> SET GLOBAL innodb_default_row_format=DYNAMIC;

mysql> SET GLOBAL innodb_default_row_format=COMPRESSED;
ERROR 1231 (42000): Variable 'innodb_default_row_format'
can't be set to the value of 'COMPRESSED'

使用 CREATE TABLEALTER TABLE 语句的 ROW_FORMAT 选项指定表的行格式:

CREATE TABLE t1 (c1 INT) ROW_FORMAT=DYNAMIC;

使用默认值的情况:

CREATE TABLE t1 (c1 INT);

CREATE TABLE t2 (c1 INT) ROW_FORMAT=DEFAULT;

如果调整了参数 innodb_default_row_format ,执行 ALTER TABLE 语句时不显示指定原行格式,则会使用新的行格式,这会导致重建表操作:

mysql> SELECT @@innodb_default_row_format;
+-----------------------------+
| @@innodb_default_row_format |
+-----------------------------+
| dynamic                     |
+-----------------------------+

mysql> CREATE TABLE t1 (c1 INT);

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE 'test/t1' \G
*************************** 1. row ***************************
     TABLE_ID: 54
         NAME: test/t1
         FLAG: 33
       N_COLS: 4
        SPACE: 35
   ROW_FORMAT: Dynamic
ZIP_PAGE_SIZE: 0
   SPACE_TYPE: Single

mysql> SET GLOBAL innodb_default_row_format=COMPACT;

mysql> ALTER TABLE t1 ADD COLUMN (c2 INT);

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE 'test/t1' \G
*************************** 1. row ***************************
     TABLE_ID: 55
         NAME: test/t1
         FLAG: 1
       N_COLS: 5
        SPACE: 36
   ROW_FORMAT: Compact
ZIP_PAGE_SIZE: 0
   SPACE_TYPE: Single

Determining the Row Format of a Table

使用 SHOW TABLE STATUS 命令查看表的行格式:

mysql> SHOW TABLE STATUS IN test1\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: 16384
      Data_free: 0
 Auto_increment: 1
    Create_time: 2016-09-14 16:29:38
    Update_time: NULL
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options:
        Comment:

查询表 INFORMATION_SCHEMA.INNODB_TABLES 获取表的行格式:

mysql> SELECT NAME, ROW_FORMAT FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME='test1/t1';
+----------+------------+
| NAME     | ROW_FORMAT |
+----------+------------+
| test1/t1 | Dynamic    |
+----------+------------+

InnoDB Disk I/O and File Space Management

InnoDB Disk I/O

InnoDB 使用 Linux 上的异步 I/O 子系统(原生 AIO)来执行数据文件页的预读和写入请求。创建多个线程来处理 I/O 操作,同时允许其他数据库操作在 I/O 仍在进行时继续进行。

Read-Ahead

如果 InnoDB 能够确定极可能很快就需要某些数据,则会执行预读操作,将数据放入缓冲池,以便在内存中可用。

具体参考:Configuring InnoDB Buffer Pool Prefetching (Read-Ahead)open in new window

Doublewrite Buffer

双写缓冲区是一个存储区域,InnoDB 在其中写入从缓冲池刷新的页,然后将页写入 InnoDB 数据文件中。如果在页写入过程中出现操作系统、存储系统或意外的 mysqld 进程退出异常,InnoDB 可以在崩溃恢复期间从双写缓冲区中找到页的正确副本。

具体参考:Doublewrite Bufferopen in new window

File Space Management

在参数文件中使用参数 innodb_data_file_path 指定 InnoDB 系统表空间数据文件。具体参数:The System Tablespaceopen in new window

默认启用参数 innodb_file_per_table 将创建的表存储在独立表空间。具体参考:File-Per-Table Tablespacesopen in new window

Pages, Extents, Segments, and Tablespaces

每个表空间都由页组成。MySQL 实例中的每个表空间都有相同的页大小,默认为 16KB。可以在创建 MySQL 实例时使用参数 innodb_page_size 进行修改。

对于 4KB,8KB,16KB 的页,组成为大小为 1MB 的区。对于 32KB 的页,区大小为 2MB。对于 64KB 的页,区大小为 4MB。表空间中的文件称为段。

InnoDB 为段最初的 32 页一次分配一页,之后为段分配整个区。对于大段,可以一次分配 4 个区。

InnoDB 为每个索引分配两个段。一个用于 B-tree 的非叶节点,另一个用于叶子节点。在磁盘上保持叶子节点连续可以实现更好的顺序 I/O 操作,因为这些叶子节点包含实际的表数据。

表空间中的一些页包含其他页的位图,因此 InnoDB 表空间中一些区不能作为一个整体分配给段,而只能作为单独的页分配给段。

Configuring the Percentage of Reserved File Segment Pages

在 MySQL 8.0.26 中引入的参数 innodb_segment_reserve_factor 指定表空间文件中保留为空页的百分比,以便 B-tree 中的页面可以连续分配。调整此参数以解决数据碎片或存储空间使用效率低下的问题。 该参数适用于独立表空间,默认为 12.5%,可以使用 SET 语句动态修改。

[(none)]> SHOW VARIABLES LIKE 'innodb_segment_reserve_factor';
+-------------------------------+-----------+
| Variable_name                 | Value     |
+-------------------------------+-----------+
| innodb_segment_reserve_factor | 12.500000 |
+-------------------------------+-----------+
1 row in set (0.00 sec)

How Pages Relate to Table Rows

对于 4KB、8KB、16KB 和 32KB 的 innodb_page_size 设置,最大行长度略小于数据库页大小的一半。例如,对于默认的 16KB 页大小,最大行长度略小于 8KB。对于 64KB 的 innodb_page_size 设置,最大行长度略小于 16KB。

如果未超过最大行长度,则行所有列都将存储在本地页中。如果超过最大行长度,则会选择可变长度列将其存储于页外(Off-Page)存储,直到该行符合最大行长度限制。可变长度列的页外(Off-Page)存储因行格式而异:

  • COMPACT 和 REDUNDANT 行格式:当选择可变长度列进行页外(Off-Page)存储时,InnoDB 将列中的前 768 个字节本地存储,剩余字节存储在溢出页(Overflow Pages)。每个这样的列都有自己的溢出页(Overflow Pages)列表。768 字节的前缀有一个 20 字节的值,存储列的真实长度,并指向溢出页(Overflow Pages)列表。参考:InnoDB Row Formatsopen in new window
  • DYNAMIC 和 COMPRESSED 行格式:当选择可变长度列进行页外(Off-Page)存储时,InnoDB 在行中本地存储一个 20 字节的指针。参考:InnoDB Row Formatsopen in new window

LONGBLOB 和 LONGTEXT 列必须小于 4GB,并且包括 BLOB 和 TEXT 列在内的总行长度必须小于 4GB。

InnoDB Checkpoints

增大日志文件可能会减少检查点期间的磁盘 I/O。

How Checkpoint Processing Works

InnoDB 实现了一种称为模糊检查点的检查点机制。InnoDB 以小批量的方式从缓冲池中刷新修改后的数据库页,而不需要一次集中刷新缓冲池,避免了在检查点过程中中断对用户 SQL 语句的处理。 在崩溃恢复期间,InnoDB 会查找写入日志文件的检查点标签,在标签之前对数据库的所有修改都存在于数据库的磁盘映像中。然后 InnoDB 从检查点向前扫描日志文件,将记录的修改应用于数据库。

Defragmenting a Table

在二级索引中进行随机插入或删除会导致索引碎片化,意味着磁盘上索引页的物理顺序与页上记录的索引顺序不接近,或者在分配给索引的 64 页块中有许多未使用的页。

碎片化的一个现象是,表占用的空间超过了其 “应该” 占用的空间。所有 InnoDB 数据和索引都存储在 B-tree 中,填充因子(fill factoropen in new window)可能在 50% 到 100% 之间变化。碎片化的另一个现象是,像对下面表扫描所花费的时间比 “应该” 花费的时间更长:

SELECT COUNT(*) FROM t WHERE non_indexed_column <> 12345;

为加快索引扫描,可以定期执行空的 ALTER TABLE 操作,重新生成表:

ALTER TABLE tbl_name ENGINE=INNODB;
ALTER TABLE tbl_name FORCE;

执行碎片整理操作的另一种方法是使用 mysqldumpopen in new window 将表转储到文本文件,删除表,然后从转储文件中重新加载。 如果索引的插入总是升序,并且只从末尾删除记录,那么 InnoDB 文件空间管理算法可以保证索引中不会出现碎片。

Reclaiming Disk Space with TRUNCATE TABLE

要在截断 InnoDB 表时回收操作系统磁盘空间,该表必须存储在自己的 ".ibd" 文件中。要将表存储在自己的 ".ibd" 文件中,必须在创建表时启用 innodb_file_per_table。此外,被截断的表和其他表之间不能有外键约束,否则 TRUNCATE TABLE 操作将失败。 当表被截断时,会被删除并在新的 ".ibd" 文件中重建,释放空间到操作系统。而截断存储在系统表空间和常规表空间内的 InnoDB 表释放的空间,操作系统无法回收使用。 截断表并将回收磁盘空间到操作系统的能力也意味着物理备份可以更小。

InnoDB and Online DDL

在线 DDL 功能提供了对即时(instant)和就地(in-place)表更改以及并发 DML 的支持:

  • 在繁忙的生产环境中提高了响应能力和可用性。
  • 对于就地操作,可以使用 LOCK 子句在 DDL 操作期间调整性能和并发性之间的平衡。
  • 与表复制方法相比,磁盘空间使用量和 I/O 开销更少。

通常,不需要做额外操作来启用在线 DDL。默认情况下,MySQL 会在允许的情况下即时或就地执行操作,并尽可能少地锁定。

可以使用 ALTER TABLE 语句的 ALGORITHMLOCK 子句来控制 DDL 操作。这些子句放在语句的末尾,用逗号分隔。例如:

ALTER TABLE tbl_name ADD PRIMARY KEY (column), ALGORITHM=INPLACE, LOCK=NONE;

LOCK 子句可用于就地执行的操作,可在操作期间微调对表的并发访问程度。对于即时执行的操作,仅支持 LOCK=DEFAULT 。ALGORITHM 子句主要用于性能比较。例如:

  • 为了避免在执行就地 ALTER TABLE 操作时意外地使表不可读、不可写或不可读写,可在 ALTER TABLE 语句中指定一个子句,如 LOCK=NONE (允许读写)或 LOCK=SHARED(允许读)。如果请求的并发级别不可用,则操作将立即停止。
  • 要比较算法之间的性能,使用 ALGORITHM=INSTANTALGORITHM=INPLACEALGORITHM=COPY 的语句。也可以启用参数 old_alter_table 强制使用 ALGORITHM=COPY

Online DDL Operations

Index Operations

对于索引的在线 DDL 操作如下表:

OperationInstantIn PlaceRebuilds TablePermits Concurrent DMLOnly Modifies Metadata
Creating or adding a secondary indexNoYesNoYesNo
Dropping an indexNoYesNoYesYes
Renaming an indexNoYesNoYesYes
Adding a FULLTEXT indexNoYes*No*NoNo
Adding a SPATIAL indexNoYesNoNoNo
Changing the index typeYesYesNoYesYes
  • 创建或增加二级索引
CREATE INDEX name ON table (col_list);
ALTER TABLE tbl_name ADD INDEX name (col_list);
  • 删除索引
DROP INDEX name ON table;
ALTER TABLE tbl_name DROP INDEX name;
  • 重命名索引
ALTER TABLE tbl_name RENAME INDEX old_index_name TO new_index_name, ALGORITHM=INPLACE, LOCK=NONE;
  • 增加 FULLTEXT 索引
CREATE FULLTEXT INDEX name ON table(column);

如果没有定义 FTS_DOC_ID 列,则添加第一个 FULLTEXT 索引将重建表。添加额外的 FULLTEXT 索引无需重建表。

  • 增加 SPATIAL索引
CREATE TABLE geom (g GEOMETRY NOT NULL);
ALTER TABLE geom ADD SPATIAL INDEX(g), ALGORITHM=INPLACE, LOCK=SHARED;
  • 修改索引类型 (USING {BTREE | HASH})
ALTER TABLE tbl_name DROP INDEX i1, ADD INDEX i1(key_part,...) USING BTREE, ALGORITHM=INSTANT;

Primary Key Operations

对于主键的在线 DDL 操作如下表:

OperationInstantIn PlaceRebuilds TablePermits Concurrent DMLOnly Modifies Metadata
Adding a primary keyNoYes*Yes*YesNo
Dropping a primary keyNoNoYesNoNo
Dropping a primary key and adding anotherNoYesYesYesNo
  • 增加主键
ALTER TABLE tbl_name ADD PRIMARY KEY (column), ALGORITHM=INPLACE, LOCK=NONE;

重组聚集索引总是需要复制表数据。因此,最好在创建表时定义主键。MySQL 通过将现有数据从原始表复制到具有所需索引结构的临时表来创建新的聚集索引。一旦数据被完全复制到临时表中,原始表就会用不同的临时表名重命名。使用原始表的名称重命名包含新聚集索引的临时表,并将原始表从数据库中删除。

尽管仍然需要复制数据,使用 ALGORITHM=INPLACEALGORITHM=COPY 更高效。

  • 删除主键
ALTER TABLE tbl_name DROP PRIMARY KEY, ALGORITHM=COPY;

只有 ALGORITHM=COPY 支持在一个 ALTER TABLE 语句中删除主键并且不添加新主键。

  • 删除主键并添加主键
ALTER TABLE tbl_name DROP PRIMARY KEY, ADD PRIMARY KEY (column), ALGORITHM=INPLACE, LOCK=NONE;

Column Operations

对于列的在线 DDL 操作如下表:

OperationInstantIn PlaceRebuilds TablePermits Concurrent DMLOnly Modifies Metadata
Adding a columnYes*YesNo*Yes*Yes
Dropping a columnYes*YesYesYesYes
Renaming a columnYes*YesNoYes*Yes
Reordering columnsNoYesYesYesNo
Setting a column default valueYesYesNoYesYes
Changing the column data typeNoNoYesNoNo
Extending VARCHAR column sizeNoYesNoYesYes
Dropping the column default valueYesYesNoYesYes
Changing the auto-increment valueNoYesNoYesNo*
Making a column NULLNoYesYes*YesNo
Making a column NOT NULLNoYes*Yes*YesNo
Modifying the definition of an ENUM or SET columnYesYesNoYesYes
  • 增加列
ALTER TABLE tbl_name ADD COLUMN column_name column_definition, ALGORITHM=INSTANT;

INSTANT 是从 MySQL 8.0.12 开始的默认算法,之前是 INPLACE

使用 INSTANT 算法增加列有如下限制:

  • 不能与其他不支持 INSTANT 算法的 ALTER TABLE 操作一起使用。
  • 在 MySQL 8.0.29 之前,只能添加到列最后;从 MySQL 8.0.29 开始,可以添加到任意位置。
  • 不支持使用 ROW_FORMAT=COMPRESSED 的表,有 FULLTEXT 索引的表,数据字典表空间的表,临时表。临时表只支持 ALGORITHM=COPY
  • 如果增加列时,最大可能行大小超过了最大允许行大小,则会报错。
  • 列数不超过1022。

可在一个 ALTER TABLE 语句中添加多列:

ALTER TABLE t1 ADD COLUMN c2 INT, ADD COLUMN c3 INT, ALGORITHM=INSTANT;

每个 ALTER TABLE ... ALGORITHM=INSTANT 操作后都会创建一个新行版本,查询 INFORMATION_SCHEMA.INNODB_TABLES.TOTAL_ROW_VERSIONS 查看行版本数量,每次即时(instant)添加或删除列时,都会递增,初始值为 0。

mysql>  SELECT NAME, TOTAL_ROW_VERSIONS FROM INFORMATION_SCHEMA.INNODB_TABLES 
        WHERE NAME LIKE 'test/t1';
+---------+--------------------+
| NAME    | TOTAL_ROW_VERSIONS |
+---------+--------------------+
| test/t1 |                  0 |
+---------+--------------------+

当通过 ALTER TABLEOPTIMIZE TABLE 操作重建具有即时(instant)添加或删除列的表时, TOTAL_ROW_VERSIONS 值将被重置为 0。允许的最大行版本数为 64,当达到此限制时,使用 ALGORITHM=INSTANTADD COLUMNDROP COLUMN 操作将被拒绝并报错:

ERROR 4080 (HY000): Maximum row versions reached for table test/t1. No more columns can be added or dropped instantly. Please use COPY/INPLACE.

查询 INFORMATION_SCHEMA 的以下字段获取额外信息:

  • INNODB_COLUMNS.DEFAULT_VALUE
  • INNODB_COLUMNS.HAS_DEFAULT
  • INNODB_TABLES.INSTANT_COLS

添加自增列时,不允许并发 DML。至少需要 ALGORITHM=INPLACE, LOCK=SHARED 。 如果使用 ALGORITHM=INPLACE 添加列,则会重建表。

  • 删除列
ALTER TABLE tbl_name DROP COLUMN column_name, ALGORITHM=INSTANT;

INSTANT 是从 MySQL 8.0.12 开始的默认算法,之前是 INPLACE

使用 INSTANT 算法删除列有如下限制:

  • 不能与其他不支持 INSTANT 算法的 ALTER TABLE 操作一起使用。

  • 不支持使用 ROW_FORMAT=COMPRESSED 的表,有 FULLTEXT 索引的表,数据字典表空间的表,临时表。临时表只支持 ALGORITHM=COPY

可在一个 ALTER TABLE 语句中删除多列:

ALTER TABLE t1 DROP COLUMN c4, DROP COLUMN c5, ALGORITHM=INSTANT;

  • 重命名列
ALTER TABLE tbl CHANGE old_col_name new_col_name data_type, ALGORITHM=INSTANT, LOCK=NONE;

从 MySQL 8.0.28 开始,ALGORITHM=INSTANT 支持重命名列。之前只能使用 ALGORITHM=INPLACEALGORITHM=COPY

要允许并发 DML,需保持相同的数据类型,只更改列名。

  • 重排列
ALTER TABLE tbl_name MODIFY COLUMN col_name column_definition FIRST, ALGORITHM=INPLACE, LOCK=NONE;

重排列需要重组数据。昂贵操作。

  • 修改列数据类型
ALTER TABLE tbl_name CHANGE c1 c1 BIGINT, ALGORITHM=COPY;

只能使用 ALGORITHM=COPY

  • 扩展 VARCHAR
ALTER TABLE tbl_name CHANGE COLUMN c1 c1 VARCHAR(255), ALGORITHM=INPLACE, LOCK=NONE;

VARCHAR 列所需的长度字节数必须保持不变。对于大小为 0 到 255 字节的 VARCHAR 列,需要一个长度字节来表示值的字节数。对于大小为 256 字节或以上的 VARCHAR 列,需要两个长度字节来表示值的字节数。因此,就地 ALTER TABLE 仅支持将 VARCHAR 列大小从 0 增加到 255 字节,或从 256 字节增加到更大。就地 ALTER TABLE 不支持将 VARCHAR 列的大小从小于 256 字节增加到等于或大于 256 字节。因为在这种情况下,所需长度字节的数量从 1 变为 2,这仅由 ALGORITHM=COPY 支持。例如,试图使用就地 ALTER TABLE 将单字节字符集的 VARCHAR 列大小从 VARCHAR(255) 更改为 VARCHAR(256) ,会返回以下错误:

ALTER TABLE tbl_name ALGORITHM=INPLACE, CHANGE COLUMN c1 c1 VARCHAR(256);
ERROR 0A000: ALGORITHM=INPLACE is not supported. Reason: Cannot change
column type INPLACE. Try ALGORITHM=COPY.

就地 ALTER TABLE 不支持减小 VARCHAR 长度,需使用 ALGORITHM=COPY

  • 设置列默认值
ALTER TABLE tbl_name ALTER COLUMN col SET DEFAULT literal, ALGORITHM=INSTANT;

仅修改表元数据。默认列值存储在数据字典中。

  • 删除列默认值
ALTER TABLE tbl ALTER COLUMN col DROP DEFAULT, ALGORITHM=INSTANT;
  • 修改自增值
ALTER TABLE table AUTO_INCREMENT=next_value, ALGORITHM=INPLACE, LOCK=NONE;

修改存储在内存中的值,而不是数据文件。

  • 列置为空
ALTER TABLE tbl_name MODIFY COLUMN column_name data_type NULL, ALGORITHM=INPLACE, LOCK=NONE;

就地重建表。昂贵操作。

  • 列置为非空
ALTER TABLE tbl_name MODIFY COLUMN column_name data_type NOT NULL, ALGORITHM=INPLACE, LOCK=NONE;

就地重建表。昂贵操作。

  • 修改 ENUMSET 列定义
CREATE TABLE t1 (c1 ENUM('a', 'b', 'c'));
ALTER TABLE t1 MODIFY COLUMN c1 ENUM('a', 'b', 'c', 'd'), ALGORITHM=INSTANT;

Foreign Key Operations

对于外键的在线 DDL 操作如下表:

OperationInstantIn PlaceRebuilds TablePermits Concurrent DMLOnly Modifies Metadata
Adding a foreign key constraintNoYes*NoYesYes
Dropping a foreign key constraintNoYesNoYesYes
  • 增加外键约束
ALTER TABLE tbl1 ADD CONSTRAINT fk_name FOREIGN KEY index (col1)
  REFERENCES tbl2(col2) referential_actions;

只有禁用参数 foreign_key_checks(默认为 ON)才能使用 INPLACE 算法,否则只能使用 COPY 算法。

  • 删除外键约束
ALTER TABLE tbl DROP FOREIGN KEY fk_name;

无论参数 foreign_key_checks 启用或禁用,都可以在线删除外键。

也可以在单个语句中删除外键及其关联索引:

ALTER TABLE table DROP FOREIGN KEY constraint, DROP INDEX index;

Table Operations

对于表的在线 DDL 操作如下表:

OperationInstantIn PlaceRebuilds TablePermits Concurrent DMLOnly Modifies Metadata
Changing the ROW_FORMATNoYesYesYesNo
Changing the KEY_BLOCK_SIZENoYesYesYesNo
Setting persistent table statisticsNoYesNoYesYes
Specifying a character setNoYesYes*YesNo
Converting a character setNoNoYes*NoNo
Optimizing a tableNoYes*YesYesNo
Rebuilding with the FORCE optionNoYes*YesYesNo
Performing a null rebuildNoYes*YesYesNo
Renaming a tableYesYesNoYesYes
  • 修改 ROW_FORMAT
ALTER TABLE tbl_name ROW_FORMAT = row_format, ALGORITHM=INPLACE, LOCK=NONE;

数据重组,昂贵操作。

  • 修改 KEY_BLOCK_SIZE
ALTER TABLE tbl_name KEY_BLOCK_SIZE = value, ALGORITHM=INPLACE, LOCK=NONE;

数据重组,昂贵操作。

  • 修改表持久统计信息选项
ALTER TABLE tbl_name STATS_PERSISTENT=0, STATS_SAMPLE_PAGES=20, STATS_AUTO_RECALC=1, ALGORITHM=INPLACE, LOCK=NONE;

仅修改表元数据。

  • 指定字符集
ALTER TABLE tbl_name CHARACTER SET = charset_name, ALGORITHM=INPLACE, LOCK=NONE;

重建表。

  • 转换字符集
ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name, ALGORITHM=COPY;

重建表。

  • 优化表
OPTIMIZE TABLE tbl_name;

具有 FULLTEXT 索引的表不支持就地操作。该操作使用 INPLACE 算法,但不允许使用 ALGORITHMLOCK 语法。

  • 使用 FORCE 选项重建表
ALTER TABLE tbl_name FORCE, ALGORITHM=INPLACE, LOCK=NONE;

具有 FULLTEXT 索引的表不支持 ALGORITHM=INPLACE

  • 执行空的 ALTER TABLE 操作重建表
ALTER TABLE tbl_name ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;
  • 重命名表
ALTER TABLE old_tbl_name RENAME TO new_tbl_name, ALGORITHM=INSTANT;

Partitioning Operations

对于表分区的在线 DDL 操作如下表:

Partitioning ClauseInstantIn PlacePermits DMLNotes
PARTITION BYopen in new windowNoNoNoPermits ALGORITHM=COPY, `LOCK={DEFAULT
ADD PARTITIONopen in new windowNoYes*Yes*`ALGORITHM=INPLACE, LOCK={DEFAULT
DROP PARTITIONopen in new windowNoYes*Yes*`ALGORITHM=INPLACE, LOCK={DEFAULT
DISCARD PARTITIONopen in new windowNoNoNoOnly permits ALGORITHM=DEFAULT, LOCK=DEFAULT
IMPORT PARTITIONopen in new windowNoNoNoOnly permits ALGORITHM=DEFAULT, LOCK=DEFAULT
TRUNCATE PARTITIONopen in new windowNoYesYesDoes not copy existing data. It merely deletes rows; it does not alter the definition of the table itself, or of any of its partitions.
COALESCE PARTITIONopen in new windowNoYes*No`ALGORITHM=INPLACE, LOCK={DEFAULT
REORGANIZE PARTITIONopen in new windowNoYes*No`ALGORITHM=INPLACE, LOCK={DEFAULT
EXCHANGE PARTITIONopen in new windowNoYesYes
ANALYZE PARTITIONopen in new windowNoYesYes
CHECK PARTITIONopen in new windowNoYesYes
OPTIMIZE PARTITIONopen in new windowNoNoNoALGORITHM and LOCK clauses are ignored. Rebuilds the entire table.
REBUILD PARTITIONopen in new windowNoYes*No`ALGORITHM=INPLACE, LOCK={DEFAULT
REPAIR PARTITIONopen in new windowNoYesYes
REMOVE PARTITIONINGopen in new windowNoNoNoPermits ALGORITHM=COPY, `LOCK={DEFAULT

Online DDL Performance and Concurrency

在线 DDL 改进了 MySQL 操作的几个方面:

  • 在 DDL 操作进行时,可以继续对表进行查询和 DML 操作,减少了对 MySQL 服务器资源的锁定和等待,从而提高了可扩展性。
  • 即时操作仅修改数据字典中的元数据,对表加排他元数据锁,允许并发 DML。
  • 避免了与表复制方法相关的磁盘 I/O 和 CPU 周期,从而最大限度地减少了数据库的总体负载,在 DDL 操作期间保持良好的性能和高吞吐量。
  • 与表复制操作相比,在线操作占用更少的缓冲池,避免了 DDL 操作后出现性能下降。

The LOCK clause

默认情况下,MySQL 在 DDL 操作期间使用尽可能少的锁。如果需要,可以为就地操作和某些复制操作指定 LOCK 子句,以强制执行更严格的锁。如果 LOCK 子句指定的锁级别低于特定 DDL 操作所允许的锁级别,则该语句将失败并返回错误。LOCK 子句按限制性从小到大描述如下:

  • LOCK=NONE:允许并发查询和 DML。
  • LOCK=SHARED:允许并发查询,但阻止 DML。
  • LOCK=DEFAULT:允许尽可能多的并发(并发查询、DML或两者兼有)。省略 LOCK 子句与指定 LOCK=DEFAULT 相同。如果不希望 DDL 语句的默认锁定级别会导致表出现任何可用性问题,请使用此子句。
  • LOCK=EXCLUSIVE:阻止并发查询和 DML。如果需要在尽可能短的时间内完成 DDL 操作,并且不需要并发查询和 DML 访问,请使用此子句。

Online DDL and Metadata Locks

在线 DDL 操作可分为三个阶段:

  • 阶段1:初始化。在初始化阶段,服务器将考虑存储引擎功能、语句中指定的操作以及用户指定的 ALGORITHMLOCK 选项,确定操作过程中允许的并发量。在此阶段,将使用共享的可升级元数据锁来保护当前表定义。
  • 阶段2:执行。在这个阶段,准备并执行语句。元数据锁是否升级为排他取决于初始化阶段评估的因素。如果需要排他元数据锁,则只在语句准备期间进行短暂的锁定。
  • 阶段3:提交表定义。在提交表定义阶段,元数据锁升级为排他,以收回旧的表定义并提交新的表定义。一旦被授予,排他元数据锁的持续时间会很短。

由于需要排他元数据锁,在线 DDL 操作可能需要等待在表上持有元数据锁的并发事务提交或回滚。在 DDL 操作之前或期间启动的事务可以对正在更改的表持有元数据锁。在长时间运行或非活动事务的情况下,在线 DDL 操作可能会在等待排他元数据锁时超时。此外,在线 DDL 操作请求的挂起的排他元数据锁会阻止表上的后续事务。

以下示例演示了一个等待排他元数据锁的在线 DDL 操作,以及挂起的元数据锁如何阻止表上的后续事务。

会话 1:

mysql> CREATE TABLE t1 (c1 INT) ENGINE=InnoDB;
mysql> START TRANSACTION;
mysql> SELECT * FROM t1;

会话 1 的 SELECT 语句对表 t1 加共享元数据锁。

会话 2:

mysql> ALTER TABLE t1 ADD COLUMN x INT, ALGORITHM=INPLACE, LOCK=NONE;

会话 2 中的在线 DDL 操作需要在表 t1 上使用排他元数据锁来提交表定义更改,必须等待会话 1 事务提交或回滚。

会话 3:

mysql> SELECT * FROM t1;

在会话 3 中发出的 SELECT 语句被阻止,等待会话 2 中 ALTER TABLE 操作请求的排他元数据锁被授予。

使用 SHOW FULL PROCESSLIST 查看状态:

mysql> SHOW FULL PROCESSLIST\G
...
*************************** 2. row ***************************
     Id: 5
   User: root
   Host: localhost
     db: test
Command: Query
   Time: 44
  State: Waiting for table metadata lock
   Info: ALTER TABLE t1 ADD COLUMN x INT, ALGORITHM=INPLACE, LOCK=NONE
...
*************************** 4. row ***************************
     Id: 7
   User: root
   Host: localhost
     db: test
Command: Query
   Time: 5
  State: Waiting for table metadata lock
   Info: SELECT * FROM t1
4 rows in set (0.00 sec)

Online DDL Performance

DDL 操作的性能在很大程度上取决于该操作是即时(instant)执行、就地(in-place)执行,还是重建表。

要比较算法之间的性能,使用 ALGORITHM=INSTANTALGORITHM=INPLACEALGORITHM=COPY 的语句。也可以启用参数 old_alter_table 强制使用 ALGORITHM=COPY

对于修改表数据的 DDL 操作,可以通过查看命令完成后显示的 “rows affected” 值来确定 DDL 操作是就地执行更改还是执行表复制。

在对大表运行 DDL 操作之前,进行如下测试步骤:

  1. 克隆表结构。
  2. 插入少量数据到克隆表。
  3. 对克隆表运行 DDL 操作。
  4. 检查 “rows affected” 值是否为零。非零值表示复制表数据。

Online DDL Space Requirements

在线 DDL 操作的磁盘空间要求概述如下。这些要求不适用于即时(instant)执行的操作。

  • 临时日志文件(Temporary log files):当在线 DDL 操作创建索引或更改表时,记录并发 DML 到临时日志文件。临时日志文件根据 innodb_sort_buffer_size(默认为 1048576)进行扩展,至最大值 innodb_online_alter_log_max_size(默认为 134217728)。如果操作花费时间过长,并且并发 DML 对表的修改过大,以至于临时日志文件的大小超过 innodb_online_alter_log_max_size,则在线 DDL 操作将失败,报 DB_ONLINE_LOG_TOO_BIG 错误,并且未提交的并发 DML 操作将回滚。参数 innodb_sort_buffer_size 还定义了临时日志文件读缓冲区和写缓冲区的大小。
  • 临时排序文件(Temporary sort files):重建表的在线 DDL 操作在创建索引期间将临时排序文件写入 MySQL 临时目录(Unix 为 $TMPDIR,Windows 为 %TEMP%,或使用 --tmpdir 指定的目录)。临时排序文件不会在包含原始表的目录中创建。每个临时排序文件需足够大到可以容纳一列数据,当其数据合并到表或索引中时,排序文件会被删除。涉及临时排序文件的操作可能需要的临时空间等于表中的数据量加上索引。如果空间不够,则会报错。如果 MySQL 临时目录不够大,可将参数 tmpdir(默认为 /tmp) 设置为其他目录。或者使用参数 innodb_tmpdir 为在线 DDL 操作定义一个单独的临时目录。
  • 中间表文件(Intermediate table files):一些重建表的在线 DDL 操作会在与原始表相同的目录中创建一个临时中间表文件。中间表文件可能需要与原始表大小相等的空间。中间表文件名以 #sql-ib 前缀开头,仅在在线 DDL 操作期间出现。

Online DDL Memory Management

创建或重建二级索引的在线 DDL 操作在索引创建的不同阶段分配临时缓冲区。从MySQL 8.0.27 引入的参数 innodb_ddl_buffer_size 指定在线 DDL 操作的最大缓冲区大小,默认为 1048576 bytes (1 MB)。每个 DDL 线程的最大缓冲区大小是最大缓冲区的大小除以 DDL 线程的数量(innodb_ddl_buffer_size/innodb_ddl_threads)。

[(none)]> SHOW VARIABLES LIKE 'innodb_ddl_buffer_size';
+------------------------+---------+
| Variable_name          | Value   |
+------------------------+---------+
| innodb_ddl_buffer_size | 1048576 |
+------------------------+---------+
1 row in set (0.01 sec)

在 MySQL 8.0.27 之前,参数 innodb_sort_buffer_size 定义了用于创建或重建二级索引的在线 DDL 操作的缓冲区大小。

Configuring Parallel Threads for Online DDL Operations

创建或重建二级索引的在线 DDL 操作包括:

  • 扫描聚簇索引并将数据写入临时排序文件
  • 对数据进行排序
  • 将临时排序文件中的排序数据加载到二级索引中

扫描聚簇索引的并行线程数由参数 innodb_parallel_read_threads 定义。默认为 4,最大为 256。扫描聚簇索引的实际线程数是 innodb_parallel_read_threads 和要扫描的索引子树的数量二者中的较小值。如果达到线程限制,会话将使用单个线程。

[(none)]> SHOW VARIABLES LIKE 'innodb_parallel_read_threads';
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_parallel_read_threads | 4     |
+------------------------------+-------+
1 row in set (0.01 sec)

排序和加载数据的并行线程数由 MySQL 8.0.27 中引入的参数 innodb_ddl_threads 指定,默认为 4。在 MySQL 8.0.27 之前,排序和加载操作是单线程的。

[(none)]> SHOW VARIABLES LIKE 'innodb_ddl_threads';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| innodb_ddl_threads | 4     |
+--------------------+-------+
1 row in set (0.00 sec)

Simplifying DDL Statements with Online DDL

对于可以在线完成的复合 DDL 操作,可以拆分为多个简单的 DDL 操作,例如:

ALTER TABLE t1 ADD INDEX i1(c1), ADD UNIQUE INDEX i2(c2),
  CHANGE c4_old_name c4_new_name INTEGER UNSIGNED;

可以拆分为:

ALTER TABLE t1 ADD INDEX i1(c1);
ALTER TABLE t1 ADD UNIQUE INDEX i2(c2);
ALTER TABLE t1 CHANGE c4_old_name c4_new_name INTEGER UNSIGNED NOT NULL;

Online DDL Failure Conditions

在线 DDL 操作失败的原因有:

  • ALGORITHM 子句指定的算法与特定类型的 DDL 操作或存储引擎不兼容。
  • LOCK 子句指定的锁级别较低(SHARED 或 NONE),与特定类型的 DDL 操作不兼容。
  • 在等待表上的排他锁时发生超时。
  • tmpdirinnodb_tmpdir 空间不足。
  • 操作花费时间过长,并且并发 DML 对表的修改过大,以至于临时日志文件的大小超过 innodb_online_alter_log_max_size,则在线 DDL 操作将失败,报 DB_ONLINE_LOG_TOO_BIG 错误。
  • 在线 DDL 操作期间的并发 DML 适用于修改前的表,不适用于修改后的表。例如在创建唯一索引期间插入了重复值。

Online DDL Limitations

在线 DDL 的限制有:

  • 在临时表上创建索引时会复制该表。
  • 如果表上存在 ON...CASCADEON...SET NULL 约束,则不允许使用 LOCK=NONE
  • 在就地在线 DDL 操作完成之前,必须等待在表上持有元数据锁的事务提交或回滚。在线 DDL 操作在执行阶段可能会短暂地请求表排他元数据锁,在更新表定义时,在操作的最后阶段总是需要排他元数据锁。因此,持有表元数据锁的事务可能会导致在线 DDL 操作被阻止或超时。
  • 无法暂停在线 DDL 操作,也不能限制对 I/O 或 CPU 的使用。
  • 长时间运行在线 DDL 操作可能会导致复制滞后。

InnoDB INFORMATION_SCHEMA Tables

InnoDB INFORMATION_SCHEMA 中的表提供有关 InnoDB 存储引擎各个方面的元数据、状态信息和统计信息。

[(none)]> SHOW TABLES FROM INFORMATION_SCHEMA LIKE 'INNODB%';
+----------------------------------------+
| Tables_in_information_schema (INNODB%) |
+----------------------------------------+
| INNODB_BUFFER_PAGE                     |
| INNODB_BUFFER_PAGE_LRU                 |
| INNODB_BUFFER_POOL_STATS               |
| INNODB_CACHED_INDEXES                  |
| INNODB_CMP                             |
| INNODB_CMP_PER_INDEX                   |
| INNODB_CMP_PER_INDEX_RESET             |
| INNODB_CMP_RESET                       |
| INNODB_CMPMEM                          |
| INNODB_CMPMEM_RESET                    |
| INNODB_COLUMNS                         |
| INNODB_DATAFILES                       |
| INNODB_FIELDS                          |
| INNODB_FOREIGN                         |
| INNODB_FOREIGN_COLS                    |
| INNODB_FT_BEING_DELETED                |
| INNODB_FT_CONFIG                       |
| INNODB_FT_DEFAULT_STOPWORD             |
| INNODB_FT_DELETED                      |
| INNODB_FT_INDEX_CACHE                  |
| INNODB_FT_INDEX_TABLE                  |
| INNODB_INDEXES                         |
| INNODB_METRICS                         |
| INNODB_SESSION_TEMP_TABLESPACES        |
| INNODB_TABLES                          |
| INNODB_TABLESPACES                     |
| INNODB_TABLESPACES_BRIEF               |
| INNODB_TABLESTATS                      |
| INNODB_TEMP_TABLE_INFO                 |
| INNODB_TRX                             |
| INNODB_VIRTUAL                         |
+----------------------------------------+
31 rows in set (0.00 sec)

InnoDB INFORMATION_SCHEMA Transaction and Locking Information

一个 INFORMATION_SCHEMA 表和两个 PERFORMANCE_SCHEMA 表能够监控 InnoDB 事务并诊断潜在的锁问题:

INNODB_TRX:提供关于 InnoDB 内当前执行的每个事务的信息,包括事务状态(是在运行还是在等待锁)、事务何时开始,以及事务正在执行的 SQL 语句。

data_locks:提供持有锁和请求锁信息。

data_lock_waits:提供等待给定锁的事务及给定的事务在等待的锁。

Using InnoDB Transaction and Locking Information

Identifying Blocking Transactions

会话 A:

BEGIN;
SELECT a FROM t FOR UPDATE;
SELECT SLEEP(100);

会话 B:

SELECT b FROM t FOR UPDATE;

会话 C:

SELECT c FROM t FOR UPDATE;

查看等待和阻塞的事务:

SELECT
  r.trx_id waiting_trx_id,
  r.trx_mysql_thread_id waiting_thread,
  r.trx_query waiting_query,
  b.trx_id blocking_trx_id,
  b.trx_mysql_thread_id blocking_thread,
  b.trx_query blocking_query
FROM       performance_schema.data_lock_waits w
INNER JOIN information_schema.innodb_trx b
  ON b.trx_id = w.blocking_engine_transaction_id
INNER JOIN information_schema.innodb_trx r
  ON r.trx_id = w.requesting_engine_transaction_id;
  
+----------------+----------------+----------------------------+-----------------+-----------------+----------------------------+
| waiting_trx_id | waiting_thread | waiting_query              | blocking_trx_id | blocking_thread | blocking_query             |
+----------------+----------------+----------------------------+-----------------+-----------------+----------------------------+
|          33067 |             10 | SELECT b FROM t FOR UPDATE |           33066 |               9 | SELECT SLEEP(100)          |
|          33068 |             11 | SELECT c FROM t FOR UPDATE |           33066 |               9 | SELECT SLEEP(100)          |
|          33068 |             11 | SELECT c FROM t FOR UPDATE |           33067 |              10 | SELECT b FROM t FOR UPDATE |
+----------------+----------------+----------------------------+-----------------+-----------------+----------------------------+

SELECT
  waiting_trx_id,
  waiting_pid,
  waiting_query,
  blocking_trx_id,
  blocking_pid,
  blocking_query
FROM sys.innodb_lock_waits;

+----------------+-------------+----------------------------+-----------------+--------------+----------------------------+
| waiting_trx_id | waiting_pid | waiting_query              | blocking_trx_id | blocking_pid | blocking_query             |
+----------------+-------------+----------------------------+-----------------+--------------+----------------------------+
|          33073 |          10 | SELECT b FROM t FOR UPDATE |           33072 |            9 | SELECT SLEEP(100)          |
|          33074 |          11 | SELECT c FROM t FOR UPDATE |           33072 |            9 | SELECT SLEEP(100)          |
|          33074 |          11 | SELECT c FROM t FOR UPDATE |           33073 |           10 | SELECT b FROM t FOR UPDATE |
+----------------+-------------+----------------------------+-----------------+--------------+----------------------------+
waiting trx idwaiting threadwaiting queryblocking trx idblocking threadblocking query
A46SELECT b FROM t FOR UPDATEA35SELECT SLEEP(100)
A57SELECT c FROM t FOR UPDATEA35SELECT SLEEP(100)
A57SELECT c FROM t FOR UPDATEA46SELECT b FROM t FOR UPDATE
  • 会话 B(trx id 为 A4,thread 为 6)和会话 C(trx id 为 A5,thread 为 7)都在等待会话 A(trx id 为 A3,thread 为 5)。
  • 会话 C(trx id 为 A5,thread 为 7)还等待会话 B(trx id 为 A4,thread 为 6)。

表 INNODB_TRX 输出示例:

trx idtrx statetrx startedtrx requested lock idtrx wait startedtrx weighttrx mysql thread idtrx query
A3RUN­NING2008-01-15 16:44:54NULLNULL25SELECT SLEEP(100)
A4LOCK WAIT2008-01-15 16:45:09A4:1:3:22008-01-15 16:45:0926SELECT b FROM t FOR UPDATE
A5LOCK WAIT2008-01-15 16:45:14A5:1:3:22008-01-15 16:45:1427SELECT c FROM t FOR UPDATE

表 data_locks 输出示例:

lock idlock trx idlock modelock typelock schemalock tablelock indexlock data
A3:1:3:2A3XRECORDtesttPRIMARY0x0200
A4:1:3:2A4XRECORDtesttPRIMARY0x0200
A5:1:3:2A5XRECORDtesttPRIMARY0x0200

表 data_lock_waits 输出示例:

requesting trx idrequested lock idblocking trx idblocking lock id
A4A4:1:3:2A3A3:1:3:2
A5A5:1:3:2A3A3:1:3:2
A5A5:1:3:2A4A4:1:3:2
Identifying a Blocking Query After the Issuing Session Becomes Idle

在定位阻塞事务时,如果发出查询的会话空闲,则会返回 NULL 值。在这种情况下,使用以下步骤来确定阻塞查询:

  1. 确认阻止事务的进程 ID。为表 sys.innodb_lock_waitsblocking_pid 字段值。
  2. 使用 blocking_pid 查询表 performance_schema.threads 获取阻塞事务的 THREAD_ID。例如:
SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_ID = 6;
  1. 使用 THREAD_ID 查询表 performance_schema.events_statements_current 获取最后执行的 SQL 语句。例如:
SELECT THREAD_ID, SQL_TEXT FROM performance_schema.events_statements_current
WHERE THREAD_ID = 28\G
  1. 如果线程最后执行的语句不足以定位阻塞原因,查询表 performance_schema.events_statements_history 获取线程最后执行的 10 条语句。例如:
SELECT THREAD_ID, SQL_TEXT FROM performance_schema.events_statements_history
WHERE THREAD_ID = 28 ORDER BY EVENT_ID;

Persistence and Consistency of InnoDB Transaction and Locking Information

  • INNODB_TRX、data_locks 和 data_lock_waits 表之间的数据可能不一致。
  • INNODB_TRX、data_locks 、data_lock_waits 表和 INFORMATION_SCHEMA.PROCESSLIST、performance_schema.threads 表之间的数据可能不一致。

InnoDB INFORMATION_SCHEMA Schema Object Tables

  • INNODB_TABLES:InnoDB 表元数据。
  • INNODB_COLUMNS:InnoDB 表列元数据。
  • INNODB_INDEXES:InnoDB 索引元数据。
  • INNODB_FIELDS:InnoDB 索引字段元数据。
  • INNODB_TABLESTATS:InnoDB 表统计信息。
  • INNODB_DATAFILES:InnoDB 数据文件信息。
  • INNODB_TABLESPACES:InnoDB 表空间元数据。
  • INNODB_TABLESPACES_BRIEF:InnoDB 表空间元数据。
  • INNODB_FOREIGN:InnoDB 表外键元数据。
  • INNODB_FOREIGN_COLS:InnoDB 表外键列元数据。

例子:表,列,索引,表空间元数据

  1. 创建测试表:
mysql> CREATE DATABASE test;

mysql> USE test;

mysql> CREATE TABLE t1 (
       col1 INT,
       col2 CHAR(10),
       col3 VARCHAR(10))
       ENGINE = InnoDB;

mysql> CREATE INDEX i1 ON t1(col1);
  1. 查询表信息:
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME='test/t1' \G
*************************** 1. row ***************************
     TABLE_ID: 71
         NAME: test/t1
         FLAG: 1
       N_COLS: 6
        SPACE: 57
   ROW_FORMAT: Compact
ZIP_PAGE_SIZE: 0
 INSTANT_COLS: 0

N_COLS: 6 表示还有 3 个隐藏列(DB_ROW_ID,DB_TRX_ID 和 DB_ROLL_PTR)

  1. 查询列信息:
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_COLUMNS where TABLE_ID = 71\G
*************************** 1. row ***************************
     TABLE_ID: 71
         NAME: col1
          POS: 0
        MTYPE: 6
       PRTYPE: 1027
          LEN: 4
  HAS_DEFAULT: 0
DEFAULT_VALUE: NULL
*************************** 2. row ***************************
     TABLE_ID: 71
         NAME: col2
          POS: 1
        MTYPE: 2
       PRTYPE: 524542
          LEN: 10
  HAS_DEFAULT: 0
DEFAULT_VALUE: NULL
*************************** 3. row ***************************
     TABLE_ID: 71
         NAME: col3
          POS: 2
        MTYPE: 1
       PRTYPE: 524303
          LEN: 10
  HAS_DEFAULT: 0
DEFAULT_VALUE: NULL

MTYPE 表示字段类型,6 = INT,2 = CHAR,1 = VARCHAR。

  1. 查询索引信息
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_INDEXES WHERE TABLE_ID = 71 \G
*************************** 1. row ***************************
       INDEX_ID: 111
           NAME: GEN_CLUST_INDEX
       TABLE_ID: 71
           TYPE: 1
       N_FIELDS: 0
        PAGE_NO: 3
          SPACE: 57
MERGE_THRESHOLD: 50
*************************** 2. row ***************************
       INDEX_ID: 112
           NAME: i1
       TABLE_ID: 71
           TYPE: 0
       N_FIELDS: 1
        PAGE_NO: 4
          SPACE: 57
MERGE_THRESHOLD: 50

TYPE 表示索引类型,1 = Clustered Index,0 = Secondary index。

  1. 查询索引字段信息
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FIELDS where INDEX_ID = 112 \G
*************************** 1. row ***************************
INDEX_ID: 112
    NAME: col1
     POS: 0
  1. 查询表所在的表空间信息
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLESPACES WHERE SPACE = 57 \G
*************************** 1. row ***************************
          SPACE: 57
          NAME: test/t1
          FLAG: 16417
    ROW_FORMAT: Dynamic
     PAGE_SIZE: 16384
 ZIP_PAGE_SIZE: 0
    SPACE_TYPE: Single
 FS_BLOCK_SIZE: 4096
     FILE_SIZE: 114688
ALLOCATED_SIZE: 98304
AUTOEXTEND_SIZE: 0
SERVER_VERSION: 8.0.23
 SPACE_VERSION: 1
    ENCRYPTION: N
         STATE: normal
  1. 查询表空间对应的数据文件
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_DATAFILES WHERE SPACE = 57 \G
*************************** 1. row ***************************
SPACE: 57
 PATH: ./test/t1.ibd
  1. 插入数据并查询表统计信息
mysql> INSERT INTO t1 VALUES(5, 'abc', 'def');
Query OK, 1 row affected (0.06 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLESTATS where TABLE_ID = 71 \G
*************************** 1. row ***************************
         TABLE_ID: 71
             NAME: test/t1
STATS_INITIALIZED: Initialized
         NUM_ROWS: 1
 CLUST_INDEX_SIZE: 1
 OTHER_INDEX_SIZE: 0
 MODIFIED_COUNTER: 1
          AUTOINC: 0
        REF_COUNT: 1

例子:外键元数据

  1. 创建数据库及父子表
mysql> CREATE DATABASE test;

mysql> USE test;

mysql> CREATE TABLE parent (id INT NOT NULL,
       PRIMARY KEY (id)) ENGINE=INNODB;

mysql> CREATE TABLE child (id INT, parent_id INT,
       INDEX par_ind (parent_id),
       CONSTRAINT fk1
       FOREIGN KEY (parent_id) REFERENCES parent(id)
       ON DELETE CASCADE) ENGINE=INNODB;
  1. 查询外键信息
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FOREIGN \G
*************************** 1. row ***************************
      ID: test/fk1
FOR_NAME: test/child
REF_NAME: test/parent
  N_COLS: 1
    TYPE: 1
  1. 查询外键列信息
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FOREIGN_COLS WHERE ID = 'test/fk1' \G
*************************** 1. row ***************************
          ID: test/fk1
FOR_COL_NAME: parent_id
REF_COL_NAME: id
         POS: 0

例子:关联数据字典表获取信息

mysql> SELECT a.NAME, a.ROW_FORMAT,
        @page_size :=
         IF(a.ROW_FORMAT='Compressed',
          b.ZIP_PAGE_SIZE, b.PAGE_SIZE)
          AS page_size,
         ROUND((@page_size * c.CLUST_INDEX_SIZE)
          /(1024*1024)) AS pk_mb,
         ROUND((@page_size * c.OTHER_INDEX_SIZE)
          /(1024*1024)) AS secidx_mb
       FROM INFORMATION_SCHEMA.INNODB_TABLES a
       INNER JOIN INFORMATION_SCHEMA.INNODB_TABLESPACES b on a.NAME = b.NAME
       INNER JOIN INFORMATION_SCHEMA.INNODB_TABLESTATS c on b.NAME = c.NAME
       WHERE a.NAME LIKE 'employees/%'
       ORDER BY a.NAME DESC;
+------------------------+------------+-----------+-------+-----------+
| NAME                   | ROW_FORMAT | page_size | pk_mb | secidx_mb |
+------------------------+------------+-----------+-------+-----------+
| employees/titles       | Dynamic    |     16384 |    20 |        11 |
| employees/salaries     | Dynamic    |     16384 |    93 |        34 |
| employees/employees    | Dynamic    |     16384 |    15 |         0 |
| employees/dept_manager | Dynamic    |     16384 |     0 |         0 |
| employees/dept_emp     | Dynamic    |     16384 |    12 |        10 |
| employees/departments  | Dynamic    |     16384 |     0 |         0 |
+------------------------+------------+-----------+-------+-----------+

InnoDB INFORMATION_SCHEMA Buffer Pool Tables

有关 InnoDB 缓冲池信息的数据字典有:

mysql> SHOW TABLES FROM INFORMATION_SCHEMA LIKE 'INNODB_BUFFER%';
+-----------------------------------------------+
| Tables_in_INFORMATION_SCHEMA (INNODB_BUFFER%) |
+-----------------------------------------------+
| INNODB_BUFFER_PAGE_LRU                        |
| INNODB_BUFFER_PAGE                            |
| INNODB_BUFFER_POOL_STATS                      |
+-----------------------------------------------+

警告:

查询 INNODB_BUFFER_PAGE 和 INNODB_BUFFER_PAGE_LRU 会影响性能,避免在生产环境执行。

例子:查询缓冲池包含系统数据的页数

mysql> SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       WHERE TABLE_NAME IS NULL OR (INSTR(TABLE_NAME, '/') = 0 AND INSTR(TABLE_NAME, '.') = 0);
+----------+
| COUNT(*) |
+----------+
|     1516 |
+----------+

例子:查询缓冲池系统数据页数,总页数及百分比

mysql> SELECT
       (SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       WHERE TABLE_NAME IS NULL OR (INSTR(TABLE_NAME, '/') = 0 AND INSTR(TABLE_NAME, '.') = 0)
       ) AS system_pages,
       (
       SELECT COUNT(*)
       FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       ) AS total_pages,
       (
       SELECT ROUND((system_pages/total_pages) * 100)
       ) AS system_page_percentage;
+--------------+-------------+------------------------+
| system_pages | total_pages | system_page_percentage |
+--------------+-------------+------------------------+
|          295 |        8192 |                      4 |
+--------------+-------------+------------------------+

例子:缓冲池系统数据类型

mysql> SELECT DISTINCT PAGE_TYPE FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       WHERE TABLE_NAME IS NULL OR (INSTR(TABLE_NAME, '/') = 0 AND INSTR(TABLE_NAME, '.') = 0);
+-------------------+
| PAGE_TYPE         |
+-------------------+
| SYSTEM            |
| IBUF_BITMAP       |
| UNKNOWN           |
| FILE_SPACE_HEADER |
| INODE             |
| UNDO_LOG          |
| ALLOCATED         |
+-------------------+

例子:查询缓冲池用户数据的页数

mysql> SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       WHERE TABLE_NAME IS NOT NULL AND TABLE_NAME NOT LIKE '%INNODB_TABLES%';
+----------+
| COUNT(*) |
+----------+
|     7897 |
+----------+

例子:查询缓冲池用户数据页数,总页数及百分比

mysql> SELECT
       (SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       WHERE TABLE_NAME IS NOT NULL AND (INSTR(TABLE_NAME, '/') > 0 OR INSTR(TABLE_NAME, '.') > 0)
       ) AS user_pages,
       (
       SELECT COUNT(*)
       FROM information_schema.INNODB_BUFFER_PAGE
       ) AS total_pages,
       (
       SELECT ROUND((user_pages/total_pages) * 100)
       ) AS user_page_percentage;
+------------+-------------+----------------------+
| user_pages | total_pages | user_page_percentage |
+------------+-------------+----------------------+
|       7897 |        8192 |                   96 |
+------------+-------------+----------------------+

例子:查询缓冲池中用户表

mysql> SELECT DISTINCT TABLE_NAME FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       WHERE TABLE_NAME IS NOT NULL AND (INSTR(TABLE_NAME, '/') > 0 OR INSTR(TABLE_NAME, '.') > 0)
       AND TABLE_NAME NOT LIKE '`mysql`.`innodb_%';
+-------------------------+
| TABLE_NAME              |
+-------------------------+
| `employees`.`salaries`  |
| `employees`.`employees` |
+-------------------------+

例子:查询索引页信息

mysql> SELECT INDEX_NAME, COUNT(*) AS Pages,
       ROUND(SUM(IF(COMPRESSED_SIZE = 0, @@GLOBAL.innodb_page_size, COMPRESSED_SIZE))/1024/1024)
       AS 'Total Data (MB)'
       FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       WHERE INDEX_NAME='emp_no' AND TABLE_NAME = '`employees`.`salaries`';
+------------+-------+-----------------+
| INDEX_NAME | Pages | Total Data (MB) |
+------------+-------+-----------------+
| emp_no     |  1609 |              25 |
+------------+-------+-----------------+

mysql> SELECT INDEX_NAME, COUNT(*) AS Pages,
       ROUND(SUM(IF(COMPRESSED_SIZE = 0, @@GLOBAL.innodb_page_size, COMPRESSED_SIZE))/1024/1024)
       AS 'Total Data (MB)'
       FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
       WHERE TABLE_NAME = '`employees`.`salaries`'
       GROUP BY INDEX_NAME;
+------------+-------+-----------------+
| INDEX_NAME | Pages | Total Data (MB) |
+------------+-------+-----------------+
| emp_no     |  1608 |              25 |
| PRIMARY    |  6086 |              95 |
+------------+-------+-----------------+

例子:查询页面占用的 LRU 列表中特定位置的数量

mysql> SELECT COUNT(LRU_POSITION) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE_LRU
       WHERE TABLE_NAME='`employees`.`employees`' AND LRU_POSITION < 3072;
+---------------------+
| COUNT(LRU_POSITION) |
+---------------------+
|                 548 |
+---------------------+

例子:查询缓冲池状态

mysql> SELECT * FROM information_schema.INNODB_BUFFER_POOL_STATS \G
*************************** 1. row ***************************
                         POOL_ID: 0
                       POOL_SIZE: 8192
                    FREE_BUFFERS: 1
                  DATABASE_PAGES: 8173
              OLD_DATABASE_PAGES: 3014
         MODIFIED_DATABASE_PAGES: 0
              PENDING_DECOMPRESS: 0
                   PENDING_READS: 0
               PENDING_FLUSH_LRU: 0
              PENDING_FLUSH_LIST: 0
                PAGES_MADE_YOUNG: 15907
            PAGES_NOT_MADE_YOUNG: 3803101
           PAGES_MADE_YOUNG_RATE: 0
       PAGES_MADE_NOT_YOUNG_RATE: 0
               NUMBER_PAGES_READ: 3270
            NUMBER_PAGES_CREATED: 13176
            NUMBER_PAGES_WRITTEN: 15109
                 PAGES_READ_RATE: 0
               PAGES_CREATE_RATE: 0
              PAGES_WRITTEN_RATE: 0
                NUMBER_PAGES_GET: 33069332
                        HIT_RATE: 0
    YOUNG_MAKE_PER_THOUSAND_GETS: 0
NOT_YOUNG_MAKE_PER_THOUSAND_GETS: 0
         NUMBER_PAGES_READ_AHEAD: 2713
       NUMBER_READ_AHEAD_EVICTED: 0
                 READ_AHEAD_RATE: 0
         READ_AHEAD_EVICTED_RATE: 0
                    LRU_IO_TOTAL: 0
                  LRU_IO_CURRENT: 0
                UNCOMPRESS_TOTAL: 0
              UNCOMPRESS_CURRENT: 0
mysql> SHOW ENGINE INNODB STATUS \G
...
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 579084
Buffer pool size   8192
Free buffers       1
Database pages     8173
Old database pages 3014
Modified db pages  0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 15907, not young 3803101
0.00 youngs/s, 0.00 non-youngs/s
Pages read 3270, created 13176, written 15109
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: 8173, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
...
mysql> SHOW STATUS LIKE 'Innodb_buffer%';
+---------------------------------------+-------------+
| Variable_name                         | Value       |
+---------------------------------------+-------------+
| Innodb_buffer_pool_dump_status        | not started |
| Innodb_buffer_pool_load_status        | not started |
| Innodb_buffer_pool_resize_status      | not started |
| Innodb_buffer_pool_pages_data         | 8173        |
| Innodb_buffer_pool_bytes_data         | 133906432   |
| Innodb_buffer_pool_pages_dirty        | 0           |
| Innodb_buffer_pool_bytes_dirty        | 0           |
| Innodb_buffer_pool_pages_flushed      | 15109       |
| Innodb_buffer_pool_pages_free         | 1           |
| Innodb_buffer_pool_pages_misc         | 18          |
| Innodb_buffer_pool_pages_total        | 8192        |
| Innodb_buffer_pool_read_ahead_rnd     | 0           |
| Innodb_buffer_pool_read_ahead         | 2713        |
| Innodb_buffer_pool_read_ahead_evicted | 0           |
| Innodb_buffer_pool_read_requests      | 33069332    |
| Innodb_buffer_pool_reads              | 558         |
| Innodb_buffer_pool_wait_free          | 0           |
| Innodb_buffer_pool_write_requests     | 11985961    |
+---------------------------------------+-------------+

InnoDB INFORMATION_SCHEMA Metrics Table

INNODB_METRICS 提供了有关 InnoDB 性能和资源相关计数器的信息。

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME="dml_inserts" \G
*************************** 1. row ***************************
           NAME: dml_inserts
      SUBSYSTEM: dml
          COUNT: 46273
      MAX_COUNT: 46273
      MIN_COUNT: NULL
      AVG_COUNT: 492.2659574468085
    COUNT_RESET: 46273
MAX_COUNT_RESET: 46273
MIN_COUNT_RESET: NULL
AVG_COUNT_RESET: NULL
   TIME_ENABLED: 2014-11-28 16:07:53
  TIME_DISABLED: NULL
   TIME_ELAPSED: 94
     TIME_RESET: NULL
         STATUS: enabled
           TYPE: status_counter
        COMMENT: Number of rows inserted

Enabling, Disabling, and Resetting Counters

使用下面参数启用,禁用和重置计数器:

  • innodb_monitor_enable:启用计数器。
SET GLOBAL innodb_monitor_enable = [counter-name|module_name|pattern|all];
  • innodb_monitor_disable:禁用计数器。
SET GLOBAL innodb_monitor_disable = [counter-name|module_name|pattern|all];
  • innodb_monitor_reset:重置计数器值为 0。
SET GLOBAL innodb_monitor_reset = [counter-name|module_name|pattern|all];
  • innodb_monitor_reset_all:重置所有计数器。在使用该参数前需要先禁用。
SET GLOBAL innodb_monitor_reset_all = [counter-name|module_name|pattern|all];

也可以在参数文件中配置 innodb_monitor_enable

[mysqld]
innodb_monitor_enable = log,metadata_table_handles_opened,metadata_table_handles_closed

只有参数 innodb_monitor_enable 可以在参数文件中配置。

注意:

计数器会影响性能,生产环境慎用。

Counters

INFORMATION_SCHEMA.INNODB_METRICS 表中查询 MySQL 中可用的计数器。默认启用的计数器对应于 SHOW ENGINE INNODB STATUS 输出中显示的计数器。

mysql> SELECT name, subsystem, status FROM INFORMATION_SCHEMA.INNODB_METRICS ORDER BY NAME;
+---------------------------------------------+---------------------+----------+
| name                                        | subsystem           | status   |
+---------------------------------------------+---------------------+----------+
| adaptive_hash_pages_added                   | adaptive_hash_index | disabled |
| adaptive_hash_pages_removed                 | adaptive_hash_index | disabled |
| adaptive_hash_rows_added                    | adaptive_hash_index | disabled |
| adaptive_hash_rows_deleted_no_hash_entry    | adaptive_hash_index | disabled |
| adaptive_hash_rows_removed                  | adaptive_hash_index | disabled |
| adaptive_hash_rows_updated                  | adaptive_hash_index | disabled |
| adaptive_hash_searches                      | adaptive_hash_index | enabled  |
| adaptive_hash_searches_btree                | adaptive_hash_index | enabled  |
| buffer_data_reads                           | buffer              | enabled  |
| buffer_data_written                         | buffer              | enabled  |
| buffer_flush_adaptive                       | buffer              | disabled |
| buffer_flush_adaptive_avg_pass              | buffer              | disabled |
| buffer_flush_adaptive_avg_time_est          | buffer              | disabled |
| buffer_flush_adaptive_avg_time_slot         | buffer              | disabled |
| buffer_flush_adaptive_avg_time_thread       | buffer              | disabled |
| buffer_flush_adaptive_pages                 | buffer              | disabled |
| buffer_flush_adaptive_total_pages           | buffer              | disabled |
| buffer_flush_avg_page_rate                  | buffer              | disabled |
| buffer_flush_avg_pass                       | buffer              | disabled |
| buffer_flush_avg_time                       | buffer              | disabled |
| buffer_flush_background                     | buffer              | disabled |
| buffer_flush_background_pages               | buffer              | disabled |
| buffer_flush_background_total_pages         | buffer              | disabled |
| buffer_flush_batches                        | buffer              | disabled |
| buffer_flush_batch_num_scan                 | buffer              | disabled |
| buffer_flush_batch_pages                    | buffer              | disabled |
| buffer_flush_batch_scanned                  | buffer              | disabled |
| buffer_flush_batch_scanned_per_call         | buffer              | disabled |
| buffer_flush_batch_total_pages              | buffer              | disabled |
| buffer_flush_lsn_avg_rate                   | buffer              | disabled |
| buffer_flush_neighbor                       | buffer              | disabled |
| buffer_flush_neighbor_pages                 | buffer              | disabled |
| buffer_flush_neighbor_total_pages           | buffer              | disabled |
| buffer_flush_n_to_flush_by_age              | buffer              | disabled |
| buffer_flush_n_to_flush_by_dirty_page       | buffer              | disabled |
| buffer_flush_n_to_flush_requested           | buffer              | disabled |
| buffer_flush_pct_for_dirty                  | buffer              | disabled |
| buffer_flush_pct_for_lsn                    | buffer              | disabled |
| buffer_flush_sync                           | buffer              | disabled |
| buffer_flush_sync_pages                     | buffer              | disabled |
| buffer_flush_sync_total_pages               | buffer              | disabled |
| buffer_flush_sync_waits                     | buffer              | disabled |
| buffer_LRU_batches_evict                    | buffer              | disabled |
| buffer_LRU_batches_flush                    | buffer              | disabled |
| buffer_LRU_batch_evict_pages                | buffer              | disabled |
| buffer_LRU_batch_evict_total_pages          | buffer              | disabled |
| buffer_LRU_batch_flush_avg_pass             | buffer              | disabled |
| buffer_LRU_batch_flush_avg_time_est         | buffer              | disabled |
| buffer_LRU_batch_flush_avg_time_slot        | buffer              | disabled |
| buffer_LRU_batch_flush_avg_time_thread      | buffer              | disabled |
| buffer_LRU_batch_flush_pages                | buffer              | disabled |
| buffer_LRU_batch_flush_total_pages          | buffer              | disabled |
| buffer_LRU_batch_num_scan                   | buffer              | disabled |
| buffer_LRU_batch_scanned                    | buffer              | disabled |
| buffer_LRU_batch_scanned_per_call           | buffer              | disabled |
| buffer_LRU_get_free_loops                   | buffer              | disabled |
| buffer_LRU_get_free_search                  | Buffer              | disabled |
| buffer_LRU_get_free_waits                   | buffer              | disabled |
| buffer_LRU_search_num_scan                  | buffer              | disabled |
| buffer_LRU_search_scanned                   | buffer              | disabled |
| buffer_LRU_search_scanned_per_call          | buffer              | disabled |
| buffer_LRU_single_flush_failure_count       | Buffer              | disabled |
| buffer_LRU_single_flush_num_scan            | buffer              | disabled |
| buffer_LRU_single_flush_scanned             | buffer              | disabled |
| buffer_LRU_single_flush_scanned_per_call    | buffer              | disabled |
| buffer_LRU_unzip_search_num_scan            | buffer              | disabled |
| buffer_LRU_unzip_search_scanned             | buffer              | disabled |
| buffer_LRU_unzip_search_scanned_per_call    | buffer              | disabled |
| buffer_pages_created                        | buffer              | enabled  |
| buffer_pages_read                           | buffer              | enabled  |
| buffer_pages_written                        | buffer              | enabled  |
| buffer_page_read_blob                       | buffer_page_io      | disabled |
| buffer_page_read_fsp_hdr                    | buffer_page_io      | disabled |
| buffer_page_read_ibuf_bitmap                | buffer_page_io      | disabled |
| buffer_page_read_ibuf_free_list             | buffer_page_io      | disabled |
| buffer_page_read_index_ibuf_leaf            | buffer_page_io      | disabled |
| buffer_page_read_index_ibuf_non_leaf        | buffer_page_io      | disabled |
| buffer_page_read_index_inode                | buffer_page_io      | disabled |
| buffer_page_read_index_leaf                 | buffer_page_io      | disabled |
| buffer_page_read_index_non_leaf             | buffer_page_io      | disabled |
| buffer_page_read_other                      | buffer_page_io      | disabled |
| buffer_page_read_rseg_array                 | buffer_page_io      | disabled |
| buffer_page_read_system_page                | buffer_page_io      | disabled |
| buffer_page_read_trx_system                 | buffer_page_io      | disabled |
| buffer_page_read_undo_log                   | buffer_page_io      | disabled |
| buffer_page_read_xdes                       | buffer_page_io      | disabled |
| buffer_page_read_zblob                      | buffer_page_io      | disabled |
| buffer_page_read_zblob2                     | buffer_page_io      | disabled |
| buffer_page_written_blob                    | buffer_page_io      | disabled |
| buffer_page_written_fsp_hdr                 | buffer_page_io      | disabled |
| buffer_page_written_ibuf_bitmap             | buffer_page_io      | disabled |
| buffer_page_written_ibuf_free_list          | buffer_page_io      | disabled |
| buffer_page_written_index_ibuf_leaf         | buffer_page_io      | disabled |
| buffer_page_written_index_ibuf_non_leaf     | buffer_page_io      | disabled |
| buffer_page_written_index_inode             | buffer_page_io      | disabled |
| buffer_page_written_index_leaf              | buffer_page_io      | disabled |
| buffer_page_written_index_non_leaf          | buffer_page_io      | disabled |
| buffer_page_written_on_log_no_waits         | buffer_page_io      | disabled |
| buffer_page_written_on_log_waits            | buffer_page_io      | disabled |
| buffer_page_written_on_log_wait_loops       | buffer_page_io      | disabled |
| buffer_page_written_other                   | buffer_page_io      | disabled |
| buffer_page_written_rseg_array              | buffer_page_io      | disabled |
| buffer_page_written_system_page             | buffer_page_io      | disabled |
| buffer_page_written_trx_system              | buffer_page_io      | disabled |
| buffer_page_written_undo_log                | buffer_page_io      | disabled |
| buffer_page_written_xdes                    | buffer_page_io      | disabled |
| buffer_page_written_zblob                   | buffer_page_io      | disabled |
| buffer_page_written_zblob2                  | buffer_page_io      | disabled |
| buffer_pool_bytes_data                      | buffer              | enabled  |
| buffer_pool_bytes_dirty                     | buffer              | enabled  |
| buffer_pool_pages_data                      | buffer              | enabled  |
| buffer_pool_pages_dirty                     | buffer              | enabled  |
| buffer_pool_pages_free                      | buffer              | enabled  |
| buffer_pool_pages_misc                      | buffer              | enabled  |
| buffer_pool_pages_total                     | buffer              | enabled  |
| buffer_pool_reads                           | buffer              | enabled  |
| buffer_pool_read_ahead                      | buffer              | enabled  |
| buffer_pool_read_ahead_evicted              | buffer              | enabled  |
| buffer_pool_read_requests                   | buffer              | enabled  |
| buffer_pool_size                            | server              | enabled  |
| buffer_pool_wait_free                       | buffer              | enabled  |
| buffer_pool_write_requests                  | buffer              | enabled  |
| compression_pad_decrements                  | compression         | disabled |
| compression_pad_increments                  | compression         | disabled |
| compress_pages_compressed                   | compression         | disabled |
| compress_pages_decompressed                 | compression         | disabled |
| cpu_n                                       | cpu                 | disabled |
| cpu_stime_abs                               | cpu                 | disabled |
| cpu_stime_pct                               | cpu                 | disabled |
| cpu_utime_abs                               | cpu                 | disabled |
| cpu_utime_pct                               | cpu                 | disabled |
| dblwr_async_requests                        | dblwr               | disabled |
| dblwr_flush_requests                        | dblwr               | disabled |
| dblwr_flush_wait_events                     | dblwr               | disabled |
| dblwr_sync_requests                         | dblwr               | disabled |
| ddl_background_drop_tables                  | ddl                 | disabled |
| ddl_log_file_alter_table                    | ddl                 | disabled |
| ddl_online_create_index                     | ddl                 | disabled |
| ddl_pending_alter_table                     | ddl                 | disabled |
| ddl_sort_file_alter_table                   | ddl                 | disabled |
| dml_deletes                                 | dml                 | enabled  |
| dml_inserts                                 | dml                 | enabled  |
| dml_reads                                   | dml                 | disabled |
| dml_system_deletes                          | dml                 | enabled  |
| dml_system_inserts                          | dml                 | enabled  |
| dml_system_reads                            | dml                 | enabled  |
| dml_system_updates                          | dml                 | enabled  |
| dml_updates                                 | dml                 | enabled  |
| file_num_open_files                         | file_system         | enabled  |
| ibuf_merges                                 | change_buffer       | enabled  |
| ibuf_merges_delete                          | change_buffer       | enabled  |
| ibuf_merges_delete_mark                     | change_buffer       | enabled  |
| ibuf_merges_discard_delete                  | change_buffer       | enabled  |
| ibuf_merges_discard_delete_mark             | change_buffer       | enabled  |
| ibuf_merges_discard_insert                  | change_buffer       | enabled  |
| ibuf_merges_insert                          | change_buffer       | enabled  |
| ibuf_size                                   | change_buffer       | enabled  |
| icp_attempts                                | icp                 | disabled |
| icp_match                                   | icp                 | disabled |
| icp_no_match                                | icp                 | disabled |
| icp_out_of_range                            | icp                 | disabled |
| index_page_discards                         | index               | disabled |
| index_page_merge_attempts                   | index               | disabled |
| index_page_merge_successful                 | index               | disabled |
| index_page_reorg_attempts                   | index               | disabled |
| index_page_reorg_successful                 | index               | disabled |
| index_page_splits                           | index               | disabled |
| innodb_activity_count                       | server              | enabled  |
| innodb_background_drop_table_usec           | server              | disabled |
| innodb_dblwr_pages_written                  | server              | enabled  |
| innodb_dblwr_writes                         | server              | enabled  |
| innodb_dict_lru_count                       | server              | disabled |
| innodb_dict_lru_usec                        | server              | disabled |
| innodb_ibuf_merge_usec                      | server              | disabled |
| innodb_master_active_loops                  | server              | disabled |
| innodb_master_idle_loops                    | server              | disabled |
| innodb_master_purge_usec                    | server              | disabled |
| innodb_master_thread_sleeps                 | server              | disabled |
| innodb_mem_validate_usec                    | server              | disabled |
| innodb_page_size                            | server              | enabled  |
| innodb_rwlock_sx_os_waits                   | server              | enabled  |
| innodb_rwlock_sx_spin_rounds                | server              | enabled  |
| innodb_rwlock_sx_spin_waits                 | server              | enabled  |
| innodb_rwlock_s_os_waits                    | server              | enabled  |
| innodb_rwlock_s_spin_rounds                 | server              | enabled  |
| innodb_rwlock_s_spin_waits                  | server              | enabled  |
| innodb_rwlock_x_os_waits                    | server              | enabled  |
| innodb_rwlock_x_spin_rounds                 | server              | enabled  |
| innodb_rwlock_x_spin_waits                  | server              | enabled  |
| lock_deadlocks                              | lock                | enabled  |
| lock_deadlock_false_positives               | lock                | enabled  |
| lock_deadlock_rounds                        | lock                | enabled  |
| lock_rec_grant_attempts                     | lock                | enabled  |
| lock_rec_locks                              | lock                | disabled |
| lock_rec_lock_created                       | lock                | disabled |
| lock_rec_lock_removed                       | lock                | disabled |
| lock_rec_lock_requests                      | lock                | disabled |
| lock_rec_lock_waits                         | lock                | disabled |
| lock_rec_release_attempts                   | lock                | enabled  |
| lock_row_lock_current_waits                 | lock                | enabled  |
| lock_row_lock_time                          | lock                | enabled  |
| lock_row_lock_time_avg                      | lock                | enabled  |
| lock_row_lock_time_max                      | lock                | enabled  |
| lock_row_lock_waits                         | lock                | enabled  |
| lock_schedule_refreshes                     | lock                | enabled  |
| lock_table_locks                            | lock                | disabled |
| lock_table_lock_created                     | lock                | disabled |
| lock_table_lock_removed                     | lock                | disabled |
| lock_table_lock_waits                       | lock                | disabled |
| lock_threads_waiting                        | lock                | enabled  |
| lock_timeouts                               | lock                | enabled  |
| log_checkpoints                             | log                 | disabled |
| log_concurrency_margin                      | log                 | disabled |
| log_flusher_no_waits                        | log                 | disabled |
| log_flusher_waits                           | log                 | disabled |
| log_flusher_wait_loops                      | log                 | disabled |
| log_flush_avg_time                          | log                 | disabled |
| log_flush_lsn_avg_rate                      | log                 | disabled |
| log_flush_max_time                          | log                 | disabled |
| log_flush_notifier_no_waits                 | log                 | disabled |
| log_flush_notifier_waits                    | log                 | disabled |
| log_flush_notifier_wait_loops               | log                 | disabled |
| log_flush_total_time                        | log                 | disabled |
| log_free_space                              | log                 | disabled |
| log_full_block_writes                       | log                 | disabled |
| log_lsn_archived                            | log                 | disabled |
| log_lsn_buf_dirty_pages_added               | log                 | disabled |
| log_lsn_buf_pool_oldest_approx              | log                 | disabled |
| log_lsn_buf_pool_oldest_lwm                 | log                 | disabled |
| log_lsn_checkpoint_age                      | log                 | disabled |
| log_lsn_current                             | log                 | disabled |
| log_lsn_last_checkpoint                     | log                 | disabled |
| log_lsn_last_flush                          | log                 | disabled |
| log_max_modified_age_async                  | log                 | disabled |
| log_max_modified_age_sync                   | log                 | disabled |
| log_next_file                               | log                 | disabled |
| log_on_buffer_space_no_waits                | log                 | disabled |
| log_on_buffer_space_waits                   | log                 | disabled |
| log_on_buffer_space_wait_loops              | log                 | disabled |
| log_on_file_space_no_waits                  | log                 | disabled |
| log_on_file_space_waits                     | log                 | disabled |
| log_on_file_space_wait_loops                | log                 | disabled |
| log_on_flush_no_waits                       | log                 | disabled |
| log_on_flush_waits                          | log                 | disabled |
| log_on_flush_wait_loops                     | log                 | disabled |
| log_on_recent_closed_wait_loops             | log                 | disabled |
| log_on_recent_written_wait_loops            | log                 | disabled |
| log_on_write_no_waits                       | log                 | disabled |
| log_on_write_waits                          | log                 | disabled |
| log_on_write_wait_loops                     | log                 | disabled |
| log_padded                                  | log                 | disabled |
| log_partial_block_writes                    | log                 | disabled |
| log_waits                                   | log                 | enabled  |
| log_writer_no_waits                         | log                 | disabled |
| log_writer_on_archiver_waits                | log                 | disabled |
| log_writer_on_file_space_waits              | log                 | disabled |
| log_writer_waits                            | log                 | disabled |
| log_writer_wait_loops                       | log                 | disabled |
| log_writes                                  | log                 | enabled  |
| log_write_notifier_no_waits                 | log                 | disabled |
| log_write_notifier_waits                    | log                 | disabled |
| log_write_notifier_wait_loops               | log                 | disabled |
| log_write_requests                          | log                 | enabled  |
| log_write_to_file_requests_interval         | log                 | disabled |
| metadata_table_handles_closed               | metadata            | disabled |
| metadata_table_handles_opened               | metadata            | disabled |
| metadata_table_reference_count              | metadata            | disabled |
| module_cpu                                  | cpu                 | disabled |
| module_dblwr                                | dblwr               | disabled |
| module_page_track                           | page_track          | disabled |
| os_data_fsyncs                              | os                  | enabled  |
| os_data_reads                               | os                  | enabled  |
| os_data_writes                              | os                  | enabled  |
| os_log_bytes_written                        | os                  | enabled  |
| os_log_fsyncs                               | os                  | enabled  |
| os_log_pending_fsyncs                       | os                  | enabled  |
| os_log_pending_writes                       | os                  | enabled  |
| os_pending_reads                            | os                  | disabled |
| os_pending_writes                           | os                  | disabled |
| page_track_checkpoint_partial_flush_request | page_track          | disabled |
| page_track_full_block_writes                | page_track          | disabled |
| page_track_partial_block_writes             | page_track          | disabled |
| page_track_resets                           | page_track          | disabled |
| purge_del_mark_records                      | purge               | disabled |
| purge_dml_delay_usec                        | purge               | disabled |
| purge_invoked                               | purge               | disabled |
| purge_resume_count                          | purge               | disabled |
| purge_stop_count                            | purge               | disabled |
| purge_truncate_history_count                | purge               | disabled |
| purge_truncate_history_usec                 | purge               | disabled |
| purge_undo_log_pages                        | purge               | disabled |
| purge_upd_exist_or_extern_records           | purge               | disabled |
| sampled_pages_read                          | sampling            | disabled |
| sampled_pages_skipped                       | sampling            | disabled |
| trx_active_transactions                     | transaction         | disabled |
| trx_allocations                             | transaction         | disabled |
| trx_commits_insert_update                   | transaction         | disabled |
| trx_nl_ro_commits                           | transaction         | disabled |
| trx_on_log_no_waits                         | transaction         | disabled |
| trx_on_log_waits                            | transaction         | disabled |
| trx_on_log_wait_loops                       | transaction         | disabled |
| trx_rollbacks                               | transaction         | disabled |
| trx_rollbacks_savepoint                     | transaction         | disabled |
| trx_rollback_active                         | transaction         | disabled |
| trx_ro_commits                              | transaction         | disabled |
| trx_rseg_current_size                       | transaction         | disabled |
| trx_rseg_history_len                        | transaction         | enabled  |
| trx_rw_commits                              | transaction         | disabled |
| trx_undo_slots_cached                       | transaction         | disabled |
| trx_undo_slots_used                         | transaction         | disabled |
| undo_truncate_count                         | undo                | disabled |
| undo_truncate_done_logging_count            | undo                | disabled |
| undo_truncate_start_logging_count           | undo                | disabled |
| undo_truncate_usec                          | undo                | disabled |
+---------------------------------------------+---------------------+----------+
314 rows in set (0.00 sec)

Counter Modules

每个计数器都与一个特定的模块相关联。模块名称可用于启用、禁用或重置特定子系统的所有计数器。例如,使用 module_dml 启用与 dml 子系统关联的所有计数器。

mysql> SET GLOBAL innodb_monitor_enable = module_dml;

mysql> SELECT name, subsystem, status FROM INFORMATION_SCHEMA.INNODB_METRICS
       WHERE subsystem ='dml';
+-------------+-----------+---------+
| name        | subsystem | status  |
+-------------+-----------+---------+
| dml_reads   | dml       | enabled |
| dml_inserts | dml       | enabled |
| dml_deletes | dml       | enabled |
| dml_updates | dml       | enabled |
+-------------+-----------+---------+

模块名称及相应 SUBSYSTEM 名称如下:

  • module_adaptive_hash (subsystem = adaptive_hash_index)
  • module_buffer (subsystem = buffer)
  • module_buffer_page (subsystem = buffer_page_io)
  • module_compress (subsystem = compression)
  • module_ddl (subsystem = ddl)
  • module_dml (subsystem = dml)
  • module_file (subsystem = file_system)
  • module_ibuf_system (subsystem = change_buffer)
  • module_icp (subsystem = icp)
  • module_index (subsystem = index)
  • module_innodb (subsystem = innodb)
  • module_lock (subsystem = lock)
  • module_log (subsystem = log)
  • module_metadata (subsystem = metadata)
  • module_os (subsystem = os)
  • module_purge (subsystem = purge)
  • module_trx (subsystem = transaction)
  • module_undo (subsystem = undo)

例子:启用,禁用,重置,查询计数器

  1. 创建表
mysql> USE test;
Database changed

mysql> CREATE TABLE t1 (c1 INT) ENGINE=INNODB;
Query OK, 0 rows affected (0.02 sec)
  1. 启用 dml_inserts 计数器
mysql> SET GLOBAL innodb_monitor_enable = dml_inserts;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT NAME, COMMENT FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME="dml_inserts";
+-------------+-------------------------+
| NAME        | COMMENT                 |
+-------------+-------------------------+
| dml_inserts | Number of rows inserted |
+-------------+-------------------------+
  1. 查询 INNODB_METRICS 获取 dml_inserts 计数器数据
mysql>  SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME="dml_inserts" \G
*************************** 1. row ***************************
           NAME: dml_inserts
      SUBSYSTEM: dml
          COUNT: 0
      MAX_COUNT: 0
      MIN_COUNT: NULL
      AVG_COUNT: 0
    COUNT_RESET: 0
MAX_COUNT_RESET: 0
MIN_COUNT_RESET: NULL
AVG_COUNT_RESET: NULL
   TIME_ENABLED: 2014-12-04 14:18:28
  TIME_DISABLED: NULL
   TIME_ELAPSED: 28
     TIME_RESET: NULL
         STATUS: enabled
           TYPE: status_counter
        COMMENT: Number of rows inserted
  1. 插入数据
mysql> INSERT INTO t1 values(1);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 values(2);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 values(3);
Query OK, 1 row affected (0.00 sec)
  1. 再次查询 INNODB_METRICS 获取 dml_inserts 计数器数据
mysql>  SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME="dml_inserts"\G
*************************** 1. row ***************************
           NAME: dml_inserts
      SUBSYSTEM: dml
          COUNT: 3
      MAX_COUNT: 3
      MIN_COUNT: NULL
      AVG_COUNT: 0.046153846153846156
    COUNT_RESET: 3
MAX_COUNT_RESET: 3
MIN_COUNT_RESET: NULL
AVG_COUNT_RESET: NULL
   TIME_ENABLED: 2014-12-04 14:18:28
  TIME_DISABLED: NULL
   TIME_ELAPSED: 65
     TIME_RESET: NULL
         STATUS: enabled
           TYPE: status_counter
        COMMENT: Number of rows inserted
  1. 重置 dml_inserts 计数器
mysql> SET GLOBAL innodb_monitor_reset = dml_inserts;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME="dml_inserts"\G
*************************** 1. row ***************************
           NAME: dml_inserts
      SUBSYSTEM: dml
          COUNT: 3
      MAX_COUNT: 3
      MIN_COUNT: NULL
      AVG_COUNT: 0.03529411764705882
    COUNT_RESET: 0
MAX_COUNT_RESET: 0
MIN_COUNT_RESET: NULL
AVG_COUNT_RESET: 0
   TIME_ENABLED: 2014-12-04 14:18:28
  TIME_DISABLED: NULL
   TIME_ELAPSED: 85
     TIME_RESET: 2014-12-04 14:19:44
         STATUS: enabled
           TYPE: status_counter
        COMMENT: Number of rows inserted
  1. 重置所有计数器值,需要先禁用计数器
mysql> SET GLOBAL innodb_monitor_disable = dml_inserts;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME="dml_inserts"\G
*************************** 1. row ***************************
           NAME: dml_inserts
      SUBSYSTEM: dml
          COUNT: 3
      MAX_COUNT: 3
      MIN_COUNT: NULL
      AVG_COUNT: 0.030612244897959183
    COUNT_RESET: 0
MAX_COUNT_RESET: 0
MIN_COUNT_RESET: NULL
AVG_COUNT_RESET: 0
   TIME_ENABLED: 2014-12-04 14:18:28
  TIME_DISABLED: 2014-12-04 14:20:06
   TIME_ELAPSED: 98
     TIME_RESET: NULL
         STATUS: disabled
           TYPE: status_counter
        COMMENT: Number of rows inserted
  1. 禁用计数器后,重置所有计数器
mysql> SET GLOBAL innodb_monitor_reset_all = dml_inserts;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME="dml_inserts"\G
*************************** 1. row ***************************
           NAME: dml_inserts
      SUBSYSTEM: dml
          COUNT: 0
      MAX_COUNT: NULL
      MIN_COUNT: NULL
      AVG_COUNT: NULL
    COUNT_RESET: 0
MAX_COUNT_RESET: NULL
MIN_COUNT_RESET: NULL
AVG_COUNT_RESET: NULL
   TIME_ENABLED: NULL
  TIME_DISABLED: NULL
   TIME_ELAPSED: NULL
     TIME_RESET: NULL
         STATUS: disabled
           TYPE: status_counter
        COMMENT: Number of rows inserted

InnoDB INFORMATION_SCHEMA Temporary Table Info Table

INNODB_TEMP_TABLE_INFO 提供有关用户创建的 InnoDB 临时表的信息。不包括优化器使用的内部临时表。

mysql> SHOW TABLES FROM INFORMATION_SCHEMA LIKE 'INNODB_TEMP%';
+---------------------------------------------+
| Tables_in_INFORMATION_SCHEMA (INNODB_TEMP%) |
+---------------------------------------------+
| INNODB_TEMP_TABLE_INFO                      |
+---------------------------------------------+

例子:表 INNODB_TEMP_TABLE_INFO 特性

  1. 创建 InnoDB 临时表
mysql> CREATE TEMPORARY TABLE t1 (c1 INT PRIMARY KEY) ENGINE=INNODB;
  1. 查询 INNODB_TEMP_TABLE_INFO
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO\G
*************************** 1. row ***************************
            TABLE_ID: 194
                NAME: #sql7a79_1_0
              N_COLS: 4
               SPACE: 182

其中 N_COLS 为 4 表示还创建了额外的 3 个隐藏列(DB_ROW_ID, DB_TRX_ID 和 DB_ROLL_PTR)。

  1. 重启 MySQL 后再次查询
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO\G

返回空。

  1. 创建临时表
mysql> CREATE TEMPORARY TABLE t1 (c1 INT PRIMARY KEY) ENGINE=INNODB;
  1. 查询 INNODB_TEMP_TABLE_INFO,可以发现 SPACE 也发生了变化
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO\G
*************************** 1. row ***************************
            TABLE_ID: 196
                NAME: #sql7b0e_1_0
              N_COLS: 4
               SPACE: 184

Retrieving InnoDB Tablespace Metadata from INFORMATION_SCHEMA.FILES

查询表 INFORMATION_SCHEMA.FILES 查看 InnoDB 表空间信息,包括独立表空间,常规表空间,系统表空间,临时表空间及 UNDO 表空间。

注意:

表 INNODB_TABLESPACES 和 INNODB_DATAFILES 仅限于提供独立表空间,常规表空间和 UNDO 表空间信息。

例子:查询 InnoDB 系统表空间信息

mysql> SELECT FILE_ID, FILE_NAME, FILE_TYPE, TABLESPACE_NAME, FREE_EXTENTS,
       TOTAL_EXTENTS,  EXTENT_SIZE, INITIAL_SIZE, MAXIMUM_SIZE, AUTOEXTEND_SIZE, DATA_FREE, STATUS ENGINE
       FROM INFORMATION_SCHEMA.FILES WHERE TABLESPACE_NAME LIKE 'innodb_system' \G
*************************** 1. row ***************************
        FILE_ID: 0
      FILE_NAME: ./ibdata1
      FILE_TYPE: TABLESPACE
TABLESPACE_NAME: innodb_system
   FREE_EXTENTS: 0
  TOTAL_EXTENTS: 12
    EXTENT_SIZE: 1048576
   INITIAL_SIZE: 12582912
   MAXIMUM_SIZE: NULL
AUTOEXTEND_SIZE: 67108864
      DATA_FREE: 4194304
         ENGINE: NORMAL

例子:查询独立表空间和常规表空间对应的文件

mysql> SELECT FILE_ID, FILE_NAME FROM INFORMATION_SCHEMA.FILES
       WHERE FILE_NAME LIKE '%.ibd%' ORDER BY FILE_ID;
    +---------+---------------------------------------+
    | FILE_ID | FILE_NAME                             |
    +---------+---------------------------------------+
    |       2 | ./mysql/plugin.ibd                    |
    |       3 | ./mysql/servers.ibd                   |
    |       4 | ./mysql/help_topic.ibd                |
    |       5 | ./mysql/help_category.ibd             |
    |       6 | ./mysql/help_relation.ibd             |
    |       7 | ./mysql/help_keyword.ibd              |
    |       8 | ./mysql/time_zone_name.ibd            |
    |       9 | ./mysql/time_zone.ibd                 |
    |      10 | ./mysql/time_zone_transition.ibd      |
    |      11 | ./mysql/time_zone_transition_type.ibd |
    |      12 | ./mysql/time_zone_leap_second.ibd     |
    |      13 | ./mysql/innodb_table_stats.ibd        |
    |      14 | ./mysql/innodb_index_stats.ibd        |
    |      15 | ./mysql/slave_relay_log_info.ibd      |
    |      16 | ./mysql/slave_master_info.ibd         |
    |      17 | ./mysql/slave_worker_info.ibd         |
    |      18 | ./mysql/gtid_executed.ibd             |
    |      19 | ./mysql/server_cost.ibd               |
    |      20 | ./mysql/engine_cost.ibd               |
    |      21 | ./sys/sys_config.ibd                  |
    |      23 | ./test/t1.ibd                         |
    |      26 | /home/user/test/test/t2.ibd           |
    +---------+---------------------------------------+

例子:查询 InnoDB 全局临时表空间文件

mysql> SELECT FILE_ID, FILE_NAME FROM INFORMATION_SCHEMA.FILES
       WHERE FILE_NAME LIKE '%ibtmp%';
+---------+-----------+
| FILE_ID | FILE_NAME |
+---------+-----------+
|      22 | ./ibtmp1  |
+---------+-----------+

例子:查询 InnoDB UNDO 表空间文件

mysql> SELECT FILE_ID, FILE_NAME FROM INFORMATION_SCHEMA.FILES
       WHERE FILE_NAME LIKE '%undo%';       
+------------+------------+
| FILE_ID    | FILE_NAME  |
+------------+------------+
| 4294967279 | ./undo_001 |
| 4294967278 | ./undo_002 |
+------------+------------+
2 rows in set (0.02 sec)

InnoDB Integration with MySQL Performance Schema

本节简要介绍了 InnoDB 与 Performance Schema 的集成。

Monitoring ALTER TABLE Progress for InnoDB Tables Using Performance Schema

可以使用 Performance Schema 监控 ALTER TABLE 语句执行。

ALTER TABLE 分为 7 个阶段,各阶段从先到后为:

  • stage/innodb/alter table (read PK and internal sort):读取主键。
  • stage/innodb/alter table (merge sort):增加索引。
  • stage/innodb/alter table (insert):增加索引。
  • stage/innodb/alter table (log apply index):应用 DML 日志。
  • stage/innodb/alter table (flush)
  • stage/innodb/alter table (log apply table):应用 DML 日志。
  • stage/innodb/alter table (end)

例子:使用 Performance Schema 监控 ALTER TABLE

  1. 启用 stage/innodb/alter% Instruments
mysql> UPDATE performance_schema.setup_instruments
       SET ENABLED = 'YES'
       WHERE NAME LIKE 'stage/innodb/alter%';
Query OK, 7 rows affected (0.00 sec)
Rows matched: 7  Changed: 7  Warnings: 0
  1. 启用 events_stages_currentevents_stages_historyevents_stages_history_long
mysql> UPDATE performance_schema.setup_consumers
       SET ENABLED = 'YES'
       WHERE NAME LIKE '%stages%';
Query OK, 3 rows affected (0.00 sec)
Rows matched: 3  Changed: 3  Warnings: 0
  1. 执行 ALTER TABLE 操作
mysql> ALTER TABLE employees.employees ADD COLUMN middle_name varchar(14) AFTER first_name;
Query OK, 0 rows affected (9.27 sec)
Records: 0  Duplicates: 0  Warnings: 0
  1. 查看 ALTER TABLE 操作进度,WORK_COMPLETED 表示已完成的工作量, WORK_ESTIMATED 表示总工作量。
mysql> SELECT EVENT_NAME, WORK_COMPLETED, WORK_ESTIMATED
       FROM performance_schema.events_stages_current;
+------------------------------------------------------+----------------+----------------+
| EVENT_NAME                                           | WORK_COMPLETED | WORK_ESTIMATED |
+------------------------------------------------------+----------------+----------------+
| stage/innodb/alter table (read PK and internal sort) |            280 |           1245 |
+------------------------------------------------------+----------------+----------------+
1 row in set (0.01 sec)

如果操作已完成,则表 events_stages_current 返回空,此时需要查询 events_stages_history

mysql> SELECT EVENT_NAME, WORK_COMPLETED, WORK_ESTIMATED
       FROM performance_schema.events_stages_history;
+------------------------------------------------------+----------------+----------------+
| EVENT_NAME                                           | WORK_COMPLETED | WORK_ESTIMATED |
+------------------------------------------------------+----------------+----------------+
| stage/innodb/alter table (read PK and internal sort) |            886 |           1213 |
| stage/innodb/alter table (flush)                     |           1213 |           1213 |
| stage/innodb/alter table (log apply table)           |           1597 |           1597 |
| stage/innodb/alter table (end)                       |           1597 |           1597 |
| stage/innodb/alter table (log apply table)           |           1981 |           1981 |
+------------------------------------------------------+----------------+----------------+
5 rows in set (0.00 sec)

如上所示,WORK_ESTIMATED 值是在 ALTER TABLE 处理过程中修改的。初始阶段完成后的预计工作量为 1213。当 ALTER TABLE 处理完成时,WORK_ESTIMATED 被设置为实际值 1981。

Monitoring InnoDB Mutex Waits Using Performance Schema

可以使用 Performance Schema 监控 Mutex Waits,具体步骤如下:

  1. 查看可用的 InnoDB Mutex Wait Instruments,默认禁用。
mysql> SELECT *
       FROM performance_schema.setup_instruments
       WHERE NAME LIKE '%wait/synch/mutex/innodb%';
+---------------------------------------------------------+---------+-------+
| NAME                                                    | ENABLED | TIMED |
+---------------------------------------------------------+---------+-------+
| wait/synch/mutex/innodb/commit_cond_mutex               | NO      | NO    |
| wait/synch/mutex/innodb/innobase_share_mutex            | NO      | NO    |
| wait/synch/mutex/innodb/autoinc_mutex                   | NO      | NO    |
| wait/synch/mutex/innodb/autoinc_persisted_mutex         | NO      | NO    |
| wait/synch/mutex/innodb/buf_pool_flush_state_mutex      | NO      | NO    |
| wait/synch/mutex/innodb/buf_pool_LRU_list_mutex         | NO      | NO    |
| wait/synch/mutex/innodb/buf_pool_free_list_mutex        | NO      | NO    |
| wait/synch/mutex/innodb/buf_pool_zip_free_mutex         | NO      | NO    |
| wait/synch/mutex/innodb/buf_pool_zip_hash_mutex         | NO      | NO    |
| wait/synch/mutex/innodb/buf_pool_zip_mutex              | NO      | NO    |
| wait/synch/mutex/innodb/cache_last_read_mutex           | NO      | NO    |
| wait/synch/mutex/innodb/dict_foreign_err_mutex          | NO      | NO    |
| wait/synch/mutex/innodb/dict_persist_dirty_tables_mutex | NO      | NO    |
| wait/synch/mutex/innodb/dict_sys_mutex                  | NO      | NO    |
| wait/synch/mutex/innodb/recalc_pool_mutex               | NO      | NO    |
| wait/synch/mutex/innodb/fil_system_mutex                | NO      | NO    |
| wait/synch/mutex/innodb/flush_list_mutex                | NO      | NO    |
| wait/synch/mutex/innodb/fts_bg_threads_mutex            | NO      | NO    |
| wait/synch/mutex/innodb/fts_delete_mutex                | NO      | NO    |
| wait/synch/mutex/innodb/fts_optimize_mutex              | NO      | NO    |
| wait/synch/mutex/innodb/fts_doc_id_mutex                | NO      | NO    |
| wait/synch/mutex/innodb/log_flush_order_mutex           | NO      | NO    |
| wait/synch/mutex/innodb/hash_table_mutex                | NO      | NO    |
| wait/synch/mutex/innodb/ibuf_bitmap_mutex               | NO      | NO    |
| wait/synch/mutex/innodb/ibuf_mutex                      | NO      | NO    |
| wait/synch/mutex/innodb/ibuf_pessimistic_insert_mutex   | NO      | NO    |
| wait/synch/mutex/innodb/log_sys_mutex                   | NO      | NO    |
| wait/synch/mutex/innodb/log_sys_write_mutex             | NO      | NO    |
| wait/synch/mutex/innodb/mutex_list_mutex                | NO      | NO    |
| wait/synch/mutex/innodb/page_zip_stat_per_index_mutex   | NO      | NO    |
| wait/synch/mutex/innodb/purge_sys_pq_mutex              | NO      | NO    |
| wait/synch/mutex/innodb/recv_sys_mutex                  | NO      | NO    |
| wait/synch/mutex/innodb/recv_writer_mutex               | NO      | NO    |
| wait/synch/mutex/innodb/redo_rseg_mutex                 | NO      | NO    |
| wait/synch/mutex/innodb/noredo_rseg_mutex               | NO      | NO    |
| wait/synch/mutex/innodb/rw_lock_list_mutex              | NO      | NO    |
| wait/synch/mutex/innodb/rw_lock_mutex                   | NO      | NO    |
| wait/synch/mutex/innodb/srv_dict_tmpfile_mutex          | NO      | NO    |
| wait/synch/mutex/innodb/srv_innodb_monitor_mutex        | NO      | NO    |
| wait/synch/mutex/innodb/srv_misc_tmpfile_mutex          | NO      | NO    |
| wait/synch/mutex/innodb/srv_monitor_file_mutex          | NO      | NO    |
| wait/synch/mutex/innodb/buf_dblwr_mutex                 | NO      | NO    |
| wait/synch/mutex/innodb/trx_undo_mutex                  | NO      | NO    |
| wait/synch/mutex/innodb/trx_pool_mutex                  | NO      | NO    |
| wait/synch/mutex/innodb/trx_pool_manager_mutex          | NO      | NO    |
| wait/synch/mutex/innodb/srv_sys_mutex                   | NO      | NO    |
| wait/synch/mutex/innodb/lock_mutex                      | NO      | NO    |
| wait/synch/mutex/innodb/lock_wait_mutex                 | NO      | NO    |
| wait/synch/mutex/innodb/trx_mutex                       | NO      | NO    |
| wait/synch/mutex/innodb/srv_threads_mutex               | NO      | NO    |
| wait/synch/mutex/innodb/rtr_active_mutex                | NO      | NO    |
| wait/synch/mutex/innodb/rtr_match_mutex                 | NO      | NO    |
| wait/synch/mutex/innodb/rtr_path_mutex                  | NO      | NO    |
| wait/synch/mutex/innodb/rtr_ssn_mutex                   | NO      | NO    |
| wait/synch/mutex/innodb/trx_sys_mutex                   | NO      | NO    |
| wait/synch/mutex/innodb/zip_pad_mutex                   | NO      | NO    |
| wait/synch/mutex/innodb/master_key_id_mutex             | NO      | NO    |
+---------------------------------------------------------+---------+-------+
  1. 在配置文件中启用上面的 Instruments。
performance-schema-instrument='wait/synch/mutex/innodb/%=ON'

禁用特定的 Instrument

performance-schema-instrument='wait/synch/mutex/innodb/fts%=OFF'

重启 MySQL,再次查看状态

mysql> SELECT *
       FROM performance_schema.setup_instruments
       WHERE NAME LIKE '%wait/synch/mutex/innodb%';
+-------------------------------------------------------+---------+-------+
| NAME                                                  | ENABLED | TIMED |
+-------------------------------------------------------+---------+-------+
| wait/synch/mutex/innodb/commit_cond_mutex             | YES     | YES   |
| wait/synch/mutex/innodb/innobase_share_mutex          | YES     | YES   |
| wait/synch/mutex/innodb/autoinc_mutex                 | YES     | YES   |
...
| wait/synch/mutex/innodb/master_key_id_mutex           | YES     | YES   |
+-------------------------------------------------------+---------+-------+
49 rows in set (0.00 sec)
  1. 修改表 setup_consumers 启用 Wait Event Consumers,默认禁用。
mysql> UPDATE performance_schema.setup_consumers
       SET enabled = 'YES'
       WHERE name like 'events_waits%';
Query OK, 3 rows affected (0.00 sec)
Rows matched: 3  Changed: 3  Warnings: 0

查看 events_waits_currentevents_waits_historyevents_waits_history_long 状态

mysql> SELECT * FROM performance_schema.setup_consumers;
+----------------------------------+---------+
| NAME                             | ENABLED |
+----------------------------------+---------+
| events_stages_current            | NO      |
| events_stages_history            | NO      |
| events_stages_history_long       | NO      |
| events_statements_current        | YES     |
| events_statements_history        | YES     |
| events_statements_history_long   | NO      |
| events_transactions_current      | YES     |
| events_transactions_history      | YES     |
| events_transactions_history_long | NO      |
| events_waits_current             | YES     |
| events_waits_history             | YES     |
| events_waits_history_long        | YES     |
| global_instrumentation           | YES     |
| thread_instrumentation           | YES     |
| statements_digest                | YES     |
+----------------------------------+---------+
15 rows in set (0.00 sec)
  1. 启用了 Instruments 和 Consumers 后,运行需要监控的负载,例如使用 mysqlslap 模拟负载。
$> ./mysqlslap --auto-generate-sql --concurrency=100 --iterations=10 
       --number-of-queries=1000 --number-char-cols=6 --number-int-cols=6;
  1. 查询等待事件数据,例如查询表 events_waits_summary_global_by_event_name
mysql> SELECT EVENT_NAME, COUNT_STAR, SUM_TIMER_WAIT/1000000000 SUM_TIMER_WAIT_MS
       FROM performance_schema.events_waits_summary_global_by_event_name
       WHERE SUM_TIMER_WAIT > 0 AND EVENT_NAME LIKE 'wait/synch/mutex/innodb/%'
       ORDER BY COUNT_STAR DESC;
+---------------------------------------------------------+------------+-------------------+
| EVENT_NAME                                              | COUNT_STAR | SUM_TIMER_WAIT_MS |
+---------------------------------------------------------+------------+-------------------+
| wait/synch/mutex/innodb/trx_mutex                       |     201111 |           23.4719 |
| wait/synch/mutex/innodb/fil_system_mutex                |      62244 |            9.6426 |
| wait/synch/mutex/innodb/redo_rseg_mutex                 |      48238 |            3.1135 |
| wait/synch/mutex/innodb/log_sys_mutex                   |      46113 |            2.0434 |
| wait/synch/mutex/innodb/trx_sys_mutex                   |      35134 |         1068.1588 |
| wait/synch/mutex/innodb/lock_mutex                      |      34872 |         1039.2589 |
| wait/synch/mutex/innodb/log_sys_write_mutex             |      17805 |         1526.0490 |
| wait/synch/mutex/innodb/dict_sys_mutex                  |      14912 |         1606.7348 |
| wait/synch/mutex/innodb/trx_undo_mutex                  |      10634 |            1.1424 |
| wait/synch/mutex/innodb/rw_lock_list_mutex              |       8538 |            0.1960 |
| wait/synch/mutex/innodb/buf_pool_free_list_mutex        |       5961 |            0.6473 |
| wait/synch/mutex/innodb/trx_pool_mutex                  |       4885 |         8821.7496 |
| wait/synch/mutex/innodb/buf_pool_LRU_list_mutex         |       4364 |            0.2077 |
| wait/synch/mutex/innodb/innobase_share_mutex            |       3212 |            0.2650 |
| wait/synch/mutex/innodb/flush_list_mutex                |       3178 |            0.2349 |
| wait/synch/mutex/innodb/trx_pool_manager_mutex          |       2495 |            0.1310 |
| wait/synch/mutex/innodb/buf_pool_flush_state_mutex      |       1318 |            0.2161 |
| wait/synch/mutex/innodb/log_flush_order_mutex           |       1250 |            0.0893 |
| wait/synch/mutex/innodb/buf_dblwr_mutex                 |        951 |            0.0918 |
| wait/synch/mutex/innodb/recalc_pool_mutex               |        670 |            0.0942 |
| wait/synch/mutex/innodb/dict_persist_dirty_tables_mutex |        345 |            0.0414 |
| wait/synch/mutex/innodb/lock_wait_mutex                 |        303 |            0.1565 |
| wait/synch/mutex/innodb/autoinc_mutex                   |        196 |            0.0213 |
| wait/synch/mutex/innodb/autoinc_persisted_mutex         |        196 |            0.0175 |
| wait/synch/mutex/innodb/purge_sys_pq_mutex              |        117 |            0.0308 |
| wait/synch/mutex/innodb/srv_sys_mutex                   |         94 |            0.0077 |
| wait/synch/mutex/innodb/ibuf_mutex                      |         22 |            0.0086 |
| wait/synch/mutex/innodb/recv_sys_mutex                  |         12 |            0.0008 |
| wait/synch/mutex/innodb/srv_innodb_monitor_mutex        |          4 |            0.0009 |
| wait/synch/mutex/innodb/recv_writer_mutex               |          1 |            0.0005 |
+---------------------------------------------------------+------------+-------------------+

以上查询返回事件名称(EVENT_NAME),等待事件次数(COUNT_STAR)以及总的等待事件(SUM_TIMER_WAIT_MS),单位为毫秒。

注意:

以上数据包含了在启动过程中产生的等待事件数据,可以在启动后立即执行 TRUNCATE 截断表 events_waits_summary_global_by_event_name,再运行负载。

mysql> TRUNCATE performance_schema.events_waits_summary_global_by_event_name;

InnoDB Monitors

InnoDB 监视器提供有关 InnoDB 内部状态的信息,用于性能调优。

InnoDB Monitor Types

有 2 种 InnoDB 监视器类型:

  • 标准 InnoDB 监视器,显示以下类型的信息:
    • 主后台线程完成的工作
    • 信号量等待
    • 外键和死锁错误的数据
    • 事务锁等待
    • 活动事务持有的表锁和记录锁
    • 挂起的 I/O 操作和相关统计信息
    • 插入缓冲区和自适应哈希索引统计信息
    • 重做日志数据
    • 缓冲池统计信息
    • 行操作数据
  • InnoDB 锁监视器,输出额外的锁信息。

Enabling InnoDB Monitors

当启用 InnoDB 监视器进行定期输出时,InnoDB 大约每 15 秒将输出写入 mysqldopen in new window 服务器标准错误输出(stderr)。

InnoDB 将监视器输出发送到 stderr,而不是 stdout 或固定大小的内存缓冲区,以避免潜在的缓冲区溢出。

在 Windows 上,除非另有配置,否则 stderr 将被定向到默认日志文件。如果要将输出定向到控制台窗口而不是错误日志,使用 --console 选项从控制台窗口中的命令提示符启动 MySQL。

在 Unix 和类 Unix 系统上,除非另有配置,否则 stderr 通常被定向到终端。参考 Error Logopen in new window

由于生成监视器输出会影响性能,故仅当实际需要查看监视器信息时,才应启用 InnoDB 监视器。此外,如果监视器输出定向到错误日志,后续忘记禁用监视器,则日志可能会变得非常大。

InnoDB 监视器输出以包含时间戳和监视器名称的标头开头。例如:

=====================================
2014-10-16 18:37:29 0x7fc2a95c1700 INNODB MONITOR OUTPUT
=====================================

InnoDB 锁监视器标头与标准 InnoDB 监视器标头一致。

使用参数 innodb_status_output 启用标准 InnoDB 监视器,使用参数 innodb_status_output_locks 启用 InnoDB 锁监视器。

[(none)]> SHOW VARIABLES LIKE 'innodb_status_output%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| innodb_status_output       | OFF   |
| innodb_status_output_locks | OFF   |
+----------------------------+-------+
2 rows in set (0.00 sec)

Enabling the Standard InnoDB Monitor

设置参数 innodb_status_outputON 启用标准 InnoDB 监视器:

SET GLOBAL innodb_status_output=ON;

设置参数 innodb_status_outputOFF 禁用标准 InnoDB 监视器:

SET GLOBAL innodb_status_output=OFF;

关闭 MySQL Server,会将参数 innodb_status_output 设置为默认 OFF

Enabling the InnoDB Lock Monitor

要输出 InnoDB 锁监视器数据,需求启用标准 InnoDB 监视器和 InnoDB 锁监视器。

设置参数 innodb_status_output_locksON 启用 InnoDB 锁监视器:

SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;

设置参数 innodb_status_output_locksOFF 禁用 InnoDB 锁监视器:

SET GLOBAL innodb_status_output_locks=OFF;

关闭 MySQL Server,会将参数 innodb_status_output_locks 设置为默认 OFF

注意:

如果启用 InnoDB 锁监视器用于 SHOW ENGINE INNODB STATUS 输出,则只需要设置参数 innodb_status_output_locksON

Obtaining Standard InnoDB Monitor Output On Demand

可以使用于 SHOW ENGINE INNODB STATUS 获取标准 InnoDB 监视器输出。

mysql> SHOW ENGINE INNODB STATUS\G

如果启用了 InnoDB 锁监视器,则 SHOW ENGINE INNODB STATUS 输出包含 InnoDB 锁监视器数据。

Directing Standard InnoDB Monitor Output to a Status File

可以在启动的时候指定参数 --innodb-status-file 将标准 InnoDB 监视器输出定向到一个状态文件,文件名为 innodb_status.pid,位于数据目录下,大约每 15 秒写入一次。

正常关闭时 InnoDB 会删除该状态文件,如果异常关闭,则需要手动删除。

基于性能和存储空间考虑,谨慎使用该参数。

InnoDB Standard Monitor and Lock Monitor Output

使用 SHOW ENGINE INNODB STATUS 语句的标准监视器输出限制为 1MB 大小。示例如下:

mysql> SHOW ENGINE INNODB STATUS\G
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
2018-04-12 15:14:08 0x7f971c063700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 4 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 15 srv_active, 0 srv_shutdown, 1122 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 24
OS WAIT ARRAY INFO: signal count 24
RW-shared spins 4, rounds 8, OS waits 4
RW-excl spins 2, rounds 60, OS waits 2
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 2.00 RW-shared, 30.00 RW-excl, 0.00 RW-sx
------------------------
LATEST FOREIGN KEY ERROR
------------------------
2018-04-12 14:57:24 0x7f97a9c91700 Transaction:
TRANSACTION 7717, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3
MySQL thread id 8, OS thread handle 140289365317376, query id 14 localhost root update
INSERT INTO child VALUES (NULL, 1), (NULL, 2), (NULL, 3), (NULL, 4), (NULL, 5), (NULL, 6)
Foreign key constraint fails for table `test`.`child`:
,
  CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`) ON DELETE
  CASCADE ON UPDATE CASCADE
Trying to add in child table, in index par_ind tuple:
DATA TUPLE: 2 fields;
 0: len 4; hex 80000003; asc     ;;
 1: len 4; hex 80000003; asc     ;;

But in parent table `test`.`parent`, in index PRIMARY,
the closest match we can find is record:
PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000004; asc     ;;
 1: len 6; hex 000000001e19; asc       ;;
 2: len 7; hex 81000001110137; asc       7;;

------------
TRANSACTIONS
------------
Trx id counter 7748
Purge done for trx's n:o < 7747 undo n:o < 0 state: running but idle
History list length 19
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421764459790000, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 7747, ACTIVE 23 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 9, OS thread handle 140286987249408, query id 51 localhost root updating
DELETE FROM t WHERE i = 1
------- TRX HAS BEEN WAITING 23 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 4 page no 4 n bits 72 index GEN_CLUST_INDEX of table `test`.`t`
trx id 7747 lock_mode X waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 6; hex 000000000202; asc       ;;
 1: len 6; hex 000000001e41; asc      A;;
 2: len 7; hex 820000008b0110; asc        ;;
 3: len 4; hex 80000001; asc     ;;

------------------
TABLE LOCK table `test`.`t` trx id 7747 lock mode IX
RECORD LOCKS space id 4 page no 4 n bits 72 index GEN_CLUST_INDEX of table `test`.`t`
trx id 7747 lock_mode X waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 6; hex 000000000202; asc       ;;
 1: len 6; hex 000000001e41; asc      A;;
 2: len 7; hex 820000008b0110; asc        ;;
 3: len 4; hex 80000001; asc     ;;

--------
FILE I/O
--------
I/O thread 0 state: waiting for i/o request (insert buffer thread)
I/O thread 1 state: waiting for i/o request (log thread)
I/O thread 2 state: waiting for i/o request (read thread)
I/O thread 3 state: waiting for i/o request (read thread)
I/O thread 4 state: waiting for i/o request (read thread)
I/O thread 5 state: waiting for i/o request (read thread)
I/O thread 6 state: waiting for i/o request (write thread)
I/O thread 7 state: waiting for i/o request (write thread)
I/O thread 8 state: waiting for i/o request (write thread)
I/O thread 9 state: waiting for i/o request (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
 ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
833 OS file reads, 605 OS file writes, 208 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
-------------------------------------
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 553253, node heap has 0 buffer(s)
Hash table size 553253, node heap has 1 buffer(s)
Hash table size 553253, node heap has 3 buffer(s)
Hash table size 553253, node heap has 0 buffer(s)
Hash table size 553253, node heap has 0 buffer(s)
Hash table size 553253, node heap has 0 buffer(s)
Hash table size 553253, node heap has 0 buffer(s)
Hash table size 553253, node heap has 0 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
---
LOG
---
Log sequence number          19643450
Log buffer assigned up to    19643450
Log buffer completed up to   19643450
Log written up to            19643450
Log flushed up to            19643450
Added dirty pages up to      19643450
Pages flushed up to          19643450
Last checkpoint at           19643450
129 log i/o's done, 0.00 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 2198863872
Dictionary memory allocated 409606
Buffer pool size   131072
Free buffers       130095
Database pages     973
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 810, created 163, written 404
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 973, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
----------------------
INDIVIDUAL BUFFER POOL INFO
----------------------
---BUFFER POOL 0
Buffer pool size   65536
Free buffers       65043
Database pages     491
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 411, created 80, written 210
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 491, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
---BUFFER POOL 1
Buffer pool size   65536
Free buffers       65052
Database pages     482
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 399, created 83, written 194
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: 482, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=5772, Main thread ID=140286437054208 , state=sleeping
Number of rows inserted 57, updated 354, deleted 4, read 4421
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

Standard Monitor Output Sections

  • Status:显示时间戳,监视器名称及每秒平均值所基于的秒数,此秒数是当前时间和上次打印 InnoDB 监视器输出之间的时间。

  • BACKGROUND THREAD:显示主后台线程完成的工作。

  • SEMAPHORES:显示等待信号量的线程,以及有关线程需要自旋或等待互斥锁(mutex)或 rw-lock 信号量次数的统计信息。大量线程等待信号量可能是由磁盘 I/O 或 InnoDB 内部争用问题导致。争用可能是由高并发查询或操作系统线程调度问题导致。在这种情况下,将参数 innodb_thread_concurrency 设置为小于默认值可能会有所帮助。 Spin rounds per wait 行显示每个操作系统等待互斥锁(mutex)的自旋次数。使用 SHOW ENGINE INNODB MUTEX 查看互斥锁(mutex)指标报告。

  • LATEST FOREIGN KEY ERROR:显示有关最近的外键约束错误信息。如果没有发生此类错误,则不存在该项。内容包括失败的语句和约束以及父子表信息。

  • LATEST DETECTED DEADLOCK:显示最近的死锁信息。如果没有发生死锁,则不存在该项。内容包括相关事务,语句及拥有和需要的锁,以及 InnoDB 决定回滚以解决死锁的事务。参考 InnoDB Lockingopen in new window

  • TRANSACTIONS:如果显示锁等待,则可能存在锁争用。输出还有助于跟踪事务死锁的原因。

  • FILE I/O:显示有关 InnoDB 用于执行各种类型 I/O 的线程信息。其中前几个专用于一般的 InnoDB 处理。还显示挂起的 I/O 操作信息和 I/O 性能的统计信息。线程数量由参数 innodb_read_io_threadsinnodb_write_io_threads 指定,参考 Configuring the Number of Background InnoDB I/O Threadsopen in new window

  • INSERT BUFFER AND ADAPTIVE HASH INDEX:显示 Change Bufferopen in new windowAdaptive Hash Indexopen in new window 状态。

  • LOG:显示有关 InnoDB 重做日志信息。内容包括当前日志序列号、刷新到磁盘的日志位置以及 InnoDB 上次执行检查点的位置。

  • BUFFER POOL AND MEMORY:显示有关读取页和写入页的统计信息,可以计算出当前查询涉及的数据文件 I/O 操作数。参考 Monitoring the Buffer Pool Using the InnoDB Standard Monitoropen in new windowBuffer Poolopen in new window

  • ROW OPERATIONS:显示主线程正在执行的操作,包括每种行操作类型的数量和速率。

InnoDB Troubleshooting

对于 InnoDB 的故障排除,参考以下准则:

Troubleshooting InnoDB I/O Problems

InnoDB I/O 问题的故障排除步骤取决于问题发生的时间:

  • 在 MySQL Server 启动期间。
  • 在正常操作期间,当 DML 或 DDL 语句由于文件系统级别的问题而失败时。

Initialization Problems

如果在 InnoDB 尝试初始化其表空间或日志文件时出现问题,删除 InnoDB 创建的所有文件:包括所有 ibdata 文件和所有重做日志文件(MySQL 8.0.30 及更高版本中的 #ib_redoN 文件或早期版本中的 ib_logfile 文件)。如果创建了任何 InnoDB 表,需同时从 MySQL 数据库目录中删除任何 .ibd 文件,然后再次尝试初始化InnoDB。

Runtime Problems

如果 InnoDB 在文件操作期间出现操作系统错误,则解决方案有:

  • 确保 InnoDB 数据文件目录和 InnoDB 日志目录存在。

  • 确保 mysqldopen in new window 具有在这些目录中创建文件的权限。

  • 确保 mysqldopen in new window 可以读取正确的 my.cnf 参数文件。

  • 确保足够的磁盘空间和磁盘配额。

  • 确保为子目录和数据文件指定的名称不冲突。

  • 仔细检查 innodb_data_home_dirinnodb_data_file_path 值的语法。特别是 innodb_data_file_path 中的任何 MAX 值都是硬限制,超过该限制会导致错误。

Forcing InnoDB Recovery

诊断数据库页损坏时,需要使用 SELECT ... INTO OUTFILE 语句从数据库中导出表。如果出现严重损坏可能会导致 SELECT * FROM tbl_name 语句或者后台进程异常退出,甚至导致 InnoDB 前滚恢复崩溃。在这种情况下,可以使用参数 innodb_force_recovery 强制启动 InnoDB 存储引擎,同时阻止后台操作运行,以便可以导出表。

[mysqld]
innodb_force_recovery = 1

警告:

仅在紧急情况下将参数 innodb_force_recovery 设置为大于 0 的值。强制 InnoDB 恢复时,应始终从 innodb_force_recovery 为 1 开始,并根据需要逐步增加该值。

参数 innodb_force_recovery 默认为 0(表示正常启动无需强制恢复),允许非零值范围为 1 到 6。较大的值包含较小值的功能。例如,值 3 包括值 1 和 2 的所有功能。

如果能够在 innodb_force_recovery 值为 3 或更小时导出表,那么只有损坏页面上的某些数据会丢失。值为 4 或更大时,数据文件可能会永久损坏。值 6 时数据库页处于过时状态,可能会给 B-trees 和其他数据库结构带来更多损坏。

InnoDB 在 innodb_force_recovery 大于 0 时阻止插入、更新或删除操作。在 innodb_force_recovery 为 4 或更大时,将 InnoDB 置于只读模式。

  • 1 (SRV_FORCE_IGNORE_CORRUPT):即使检测到损坏页,也保持服务运行。使用 SELECT * FROM tbl_name 跳过损坏的索引记录和页,导出表。
  • 2 (SRV_FORCE_NO_BACKGROUND):阻止主线程和任何清除线程运行。
  • 3 (SRV_FORCE_NO_TRX_UNDO):崩溃恢复后不运行事务回滚。
  • 4 (SRV_FORCE_NO_IBUF_MERGE):阻止写缓冲区合并操作。不计算表统计信息。此值可能会永久损坏数据文件。使用此值后,请准备好删除并重新创建所有二级索引。此值将 InnoDB 设置为只读。
  • 5 (SRV_FORCE_NO_UNDO_LOG_SCAN):启动数据库时不查看 UNDO 日志:InnoDB 将未完成的事务视为已提交。此值可能会永久损坏数据文件。此值将 InnoDB 设置为只读。
  • 6 (SRV_FORCE_NO_LOG_REDO):不执行与恢复相关的重做日志前滚。此值可能会永久损坏数据文件。使数据库页处于过时状态,可能会给 B-trees 和其他数据库结构带来更多损坏。此值将 InnoDB 设置为只读。

innodb_force_recovery 值为 3 或更小时,可以删除或创建表。

如果知道哪个表在回滚时导致意外退出,则可以将其删除。如果遇到由批量导入失败或 ALTER TABLE 失败导致的失控回滚,可以杀掉 mysqldopen in new window 进程并将 innodb_force_recovery 设置为 3 以在不回滚的情况下启动数据库,然后删除导致失控回滚的表。

如果表数据中的损坏阻止导出整个表,则具有 ORDER BY primary_key DESC 子句的查询可能能够导出损坏部分之后的表数据。

如果需要大的 innodb_force_recovery 值才能启动 InnoDB,则可能存在损坏的数据结构,可能会导致复杂查询(包含 WHEREORDER BY 或其他子句的查询)失败。在这种情况下,可能只能运行基本的 SELECT * FROM t 查询。

Troubleshooting InnoDB Data Dictionary Operations

表定义信息存储在 InnoDB 数据字典中。如果移动数据文件,则字典数据会变得不一致。

如果数据字典损坏或一致性问题阻止 InnoDB 启动,参考 Forcing InnoDB Recoveryopen in new window 获取有关手动恢复的信息。

Cannot Open Datafile

当启用参数 innodb_file_per_table(默认启用)时,如果独立表空间文件(.ibd 文件)丢失,在启动时可能会出现如下信息:

[ERROR] InnoDB: Operating system error number 2 in a file operation.
[ERROR] InnoDB: The error means the system cannot find the path specified.
[ERROR] InnoDB: Cannot open datafile for read-only: './test/t1.ibd' OS error: 71
[Warning] InnoDB: Ignoring tablespace `test/t1` because it could not be opened.

可以使用 DROP TABLE 语句从数据字典中删除丢失的表的数据,解决该问题。

InnoDB Error Handling

对于 InnoDB 错误,有时只回滚失败的语句,有时回滚整个事务,具体如下:

  • 如果表空间中的文件空间不足,则会发生 Table is full 错误,并且 InnoDB 会回滚 SQL 语句。

  • 事务死锁会导致 InnoDB 回滚整个事务,发生这种情况时,请重试整个事务。执行的语句等待获取锁但超时会导致 InnoDB 回滚当前语句。如果要回滚整个事务,需要启用参数 innodb_rollback_on_timeout

  • 如果没有在语句中指定 IGNORE 选项,则重复键错误将回滚 SQL 语句。

  • row too long error 将回滚 SQL 语句。

  • 其他错误主要由 MySQL 代码层检测到,回滚相应的SQL语句。

在隐式回滚期间,以及显式执行 ROLLBACK 语句期间,SHOW PROCESSLIST 在相关连接的 State 列中显示 Rolling back

InnoDB Limits

InnoDB 对表,索引,表空间及其他方面的限制如下:

  • 表最多有 1017 列。
  • 表最多有 64 个二级索引。
  • 对于 16KB 页,使用 DYNAMIC 或 COMPRESSED 行格式的 InnoDB 表索引前缀限制为 3072 字节。
  • 对于 16KB 页,使用 REDUNDANT 或 COMPACT 行格式的 InnoDB 表索引前缀限制为 767 字节。
  • 复合索引最多包含 16 列。
  • 排除页外(Off-Page)存储的任何可变长度字段,对于 4KB、8KB、16KB 和 32KB 的 innodb_page_size 设置,最大行长度略小于数据库页大小的一半。对于 64KB 的 innodb_page_size 设置,最大行长度略小于 16KB。LONGBLOB 和 LONGTEXT 列必须小于 4GB,并且包括 BLOB 和 TEXT 列在内的总行长度必须小于 4GB。如果未超过最大行长度,则行所有列都将存储在本地页中。如果超过最大行长度,则会选择可变长度列将其存储于页外(Off-Page)存储,直到该行符合最大行长度限制。参考 File Space Managementopen in new window
  • 表所有字段总长度限制为 65535 字节。
  • 重做日志总大小不超过 512 GB。
  • 表空间最小为 10 MB,最大如下表:
InnoDB Page SizeMaximum Tablespace Size
4KB16TB
8KB32TB
16KB64TB
32KB128TB
64KB256TB
  • 支持 2^32 (4294967296) 表空间。

  • 共享表空间支持 2^32 (4294967296) 表。

InnoDB Restrictions and Limitations

InnoDB 存储引擎限制如下:

  • 字段名称不能定义为内部字段名称(包括 DB_ROW_ID,DB_TRX_ID 和 DB_ROLL_PTR)。

  • SHOW TABLE STATUS 不提供准确的统计信息,行数只是估计值。

  • ROW_FORMAT=COMPRESSED 不支持大于 16 KB 的页。

  • 使用特定 InnoDB 页大小的 MySQL 实例不能使用其他使用不同页面大小的实例中的数据文件或日志文件。

上次编辑于:
贡献者: stonebox