InnoDB Storage Engine
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 使用可插拔存储引擎架构,架构图如下:
使用 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。
- 支持外键约束。
Feature | Support |
---|---|
B-tree indexes | Yes |
Backup/point-in-time recovery (Implemented in the server, rather than in the storage engine.) | Yes |
Cluster database support | No |
Clustered indexes | Yes |
Compressed data | Yes |
Data caches | Yes |
Encrypted data | Yes (Implemented in the server via encryption functions; In MySQL 5.7 and later, data-at-rest encryption is supported.) |
Foreign key support | Yes |
Full-text search indexes | Yes (Support for FULLTEXT indexes is available in MySQL 5.6 and later.) |
Geospatial data type support | Yes |
Geospatial indexing support | Yes (Support for geospatial indexing is available in MySQL 5.7 and later.) |
Hash indexes | No (InnoDB utilizes hash indexes internally for its Adaptive Hash Index feature.) |
Index caches | Yes |
Locking granularity | Row |
MVCC | Yes |
Replication support (Implemented in the server, rather than in the storage engine.) | Yes |
Storage limits | 64TB |
T-tree indexes | No |
Transactions | Yes |
Update statistics for data dictionary | Yes |
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 In-Memory Structures
Buffer Pool
缓冲池是用于缓存表和索引数据的内存区域。缓冲池允许直接从内存访问常用数据,从而加快处理速度。通常将多达 80% 的物理内存分配给缓冲池。
Buffer Pool LRU Algorithm
使用变种的 LRU 算法将缓冲池作为列表(List)进行管理。当需要空间将新页添加到缓冲池时,会收回最近最少使用的页,并在列表中点(Midpoint)添加一个新页。此策略将列表分为两个子列表:
- 在头部,是最近访问的新页的子列表
- 在尾部,是最近访问较少的旧页的子列表
该算法将频繁使用的页保留在新子列表(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 标准监视器输出以来经过的时间。
各个指标的含义如下:
Name | Description |
---|---|
Total memory allocated | The total memory allocated for the buffer pool in bytes. |
Dictionary memory allocated | The total memory allocated for the InnoDB data dictionary in bytes. |
Buffer pool size | The total size in pages allocated to the buffer pool. |
Free buffers | The total size in pages of the buffer pool free list. |
Database pages | The total size in pages of the buffer pool LRU list. |
Old database pages | The total size in pages of the buffer pool old LRU sublist. |
Modified db pages | The current number of pages modified in the buffer pool. |
Pending reads | The number of buffer pool pages waiting to be read into the buffer pool. |
Pending writes LRU | The number of old dirty pages within the buffer pool to be written from the bottom of the LRU list. |
Pending writes flush list | The number of buffer pool pages to be flushed during checkpointing. |
Pending writes single page | The number of pending independent page writes within the buffer pool. |
Pages made young | The total number of pages made young in the buffer pool LRU list (moved to the head of sublist of “new” pages). |
Pages made not young | The 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/s | The 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/s | The 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 read | The total number of pages read from the buffer pool. |
Pages created | The total number of pages created within the buffer pool. |
Pages written | The total number of pages written from the buffer pool. |
reads/s | The per second average number of buffer pool page reads per second. |
creates/s | The average number of buffer pool pages created per second. |
writes/s | The average number of buffer pool page writes per second. |
Buffer pool hit rate | The buffer pool page hit rate for pages read from the buffer pool vs from disk storage. |
young-making rate | The 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 ahead | The per second average of read ahead operations. |
Pages evicted without access | The per second average of the pages evicted without being accessed from the buffer pool. |
Random read ahead | The per second average of random read ahead operations. |
LRU len | The total size in pages of the buffer pool LRU list. |
unzip_LRU len | The length (in pages) of the buffer pool unzip_LRU list. |
I/O sum | The total number of buffer pool LRU list pages accessed. |
I/O cur | The total number of buffer pool LRU list pages accessed in the current interval. |
I/O unzip sum | The total number of buffer pool unzip_LRU list pages decompressed. |
I/O unzip cur | The 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 访问,提高了性能。
Configuring Change Buffering
写缓冲适用于大数据量的 DML 操作,例如批量插入。
使用参数 innodb_change_buffering
指定写缓冲适用的 DML 操作,默认为 all
。
Value | Numeric Value | Description |
---|---|---|
none | 0 | Do not buffer any operations. |
inserts | 1 | Buffer insert operations. |
deletes | 2 | Buffer delete marking operations; strictly speaking, the writes that mark index records for later deletion during a purge operation. |
changes | 3 | Buffer inserts and delete-marking operations. |
purges | 4 | Buffer the physical deletion operations that happen in the background. |
all | 5 | The 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_INDEX
和 IBUF_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 TABLE
的 ROW_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
迁移非分区表的步骤:
- 在目标环境,创建与原环境相同的表,可以使用
SHOW CREATE TABLE
语句获取表定义。
mysql> USE test;
mysql> CREATE TABLE t1 (c1 INT) ENGINE=INNODB;
- 在目标环境,丢弃刚刚为表创建的表空间。
mysql> ALTER TABLE t1 DISCARD TABLESPACE;
- 在源环境,使用
FLUSH TABLES ... FOR EXPORT
语句静默要迁移的表,只允许在该表上进行只读事务。该语句运行时,InnoDB 在表的模式目录中生成一个.cfg
元数据文件,该文件包含在导入操作期间用于模式验证的元数据。
mysql> USE test;
mysql> FLUSH TABLES t1 FOR EXPORT;
- 在源环境,将
.ibd
文件和.cfg
元数据文件复制到目标环境,权限需要保持一致。如果目标环境是主从架构,则需要将文件复制到主库和从库。
$> scp /path/to/datadir/test/t1.{ibd,cfg} destination-server:/path/to/datadir/test
注意:如果是加密表空间,还需要拷贝
.cfp
文件。
- 在源环境,使用
UNLOCK TABLES
释放FLUSH TABLES ... FOR EXPORT
语句获取的锁,并移除.cfg
文件。
mysql> USE test;
mysql> UNLOCK TABLES;
- 在目标环境,导入表空间。
mysql> USE test;
mysql> ALTER TABLE t1 IMPORT TABLESPACE;
Importing Partitioned Tables
迁移分区表的步骤:
- 在目标环境,创建与原环境相同的分区表,可以使用
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
- 在目标环境,丢弃刚刚为分区表创建的表空间。
mysql> ALTER TABLE t1 DISCARD TABLESPACE;
- 在源环境,使用
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
- 在源环境,将
.ibd
文件和.cfg
元数据文件复制到目标环境,权限需要保持一致。如果目标环境是主从架构,则需要将文件复制到主库和从库。
$>scp /path/to/datadir/test/t1*.{ibd,cfg} destination-server:/path/to/datadir/test
注意:如果是加密表空间,还需要拷贝
.cfp
文件。
- 在源环境,使用
UNLOCK TABLES
释放FLUSH TABLES ... FOR EXPORT
语句获取的锁,并移除.cfg
文件。
mysql> USE test;
mysql> UNLOCK TABLES;
- 在目标环境,导入表空间。
mysql> USE test;
mysql> ALTER TABLE t1 IMPORT TABLESPACE;
Importing Table Partitions
迁移表分区的步骤:
- 在目标环境,创建与原环境相同的分区表,可以使用
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
- 在目标环境,丢弃需要迁移的表分区,将会移除对应的
.ibd
文件。
mysql> ALTER TABLE t1 DISCARD PARTITION p2, p3 TABLESPACE;
mysql> \! ls /path/to/datadir/test/
t1#p#p0.ibd t1#p#p1.ibd
- 在源环境,使用
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
- 在源环境,将
.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
文件。
- 在源环境,使用
UNLOCK TABLES
释放FLUSH TABLES ... FOR EXPORT
语句获取的锁,并移除.cfg
文件。
mysql> USE test;
mysql> UNLOCK TABLES;
- 在目标环境,导入表分区。
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”:可以预先确定要插入的行数的语句。包括没有嵌套子查询的单行或多行
INSERT
和REPLACE
语句。 - “Bulk inserts”:事先不知道要插入的行数(以及所需的自增数)的语句。包括
INSERT ... SELECT
,REPLACE ... SELECT
和LOAD 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
通过添加另一个数据文件来增加系统表空间大小的步骤:
- 停止 MySQL Server。
- 如果参数
innodb_data_file_path
中最后一个数据文件定义了自动扩展属性,则移除该属性,并根据文件当前大小修改大小属性。 - 向参数
innodb_data_file_path
中增加一个数据文件,可以加上自动扩展属性,该属性只能加在最后一个数据文件上。 - 启动 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_001
和 undo_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_001
和 innodb_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_dir
,innodb_undo_directory
和 datadir
中的目录添加到参数 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_truncate
为 ON
启用自动截断 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_dir
,innodb_undo_directory
和 datadir
中的目录添加到参数 innodb_directories
。
移动表空间文件或目录的步骤:
- 停止 MySQL Server。
- 移动表空间文件或者目录。
- 调整对应参数,如果移动了独立表空间文件,则其模式目录需要保持不变。
- 启动 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 Size | Minimum AUTOEXTEND_SIZE |
---|---|
4K | 4M |
8K | 4M |
16K | 4M |
32K | 8M |
64K | 16M |
默认 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_group
和 innodb_log_file_size
。当设置了 innodb_redo_log_capacity
时,将忽略 innodb_log_files_in_group
和 innodb_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_logfile0
和 ib_logfile1
,并以循环方式写入这些文件。
修改重做日志文件的数量和大小的步骤:
- 关闭 MySQL Server。
- 修改参数文件。
[mysqld]
innodb_log_file_size=2G
innodb_log_files_in_group = 3
- 启动 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;…]';
注意:
- 目录需要预先存在。
- 目录权限必须限定,不能任何人都能访问。
- 目录不能是
datadir
,innodb_data_home_dir
,innodb_directories
,innodb_log_group_home_dir
,innodb_temp_tablespaces_dir
,innodb_tmpdir innodb_undo_directory
,secure_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 实例中,可避免重做日志写入和双写缓冲,从而加快数据加载速度。
具体步骤如下:
- 授予权限。
mysql> GRANT INNODB_REDO_LOG_ENABLE ON *.* to 'data_load_admin';
- 禁用重做日志。
mysql> ALTER INSTANCE DISABLE INNODB REDO_LOG;
- 查看状态。
mysql> SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_enabled';
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| Innodb_redo_log_enabled | OFF |
+-------------------------+-------+
- 加载数据。
- 启用重做日志。
mysql> ALTER INSTANCE ENABLE INNODB REDO_LOG;
- 查看状态。
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 Size | Number 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 锁。
兼容性列表:
X | IX | S | IS | |
---|---|---|---|---|
X | Conflict | Conflict | Conflict | Conflict |
IX | Conflict | Compatible | Conflict | Compatible |
S | Conflict | Conflict | Compatible | Compatible |
IS | Conflict | Compatible | Compatible | Compatible |
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 UPDATE
,UPDATE
,DELETE
语句:
- 如果使用唯一索引作为等值查询条件时,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 UPDATE
,UPDATE
,DELETE
语句,InnoDB 仅锁定索引记录,不锁定它们之前的间隙,可能会出现幻读。间隙锁仅用于外键约束检查和重复键检查。
READ COMMITTED 隔离级别仅支持 row-based 的二进制日志。如果在 READ COMMIT 隔离级别下使用 binlog_format=MIXED
,则将自动使用 row-based 日志记录。
对于 UPDATE
,DELETE
语句,InnoDB 仅锁定更新或删除的行,大大降低了死锁的可能性。
autocommit, Commit, and Rollback
MySQL 默认启用自动提交(autocommit),每个 SQL 语句作为一个事务,如果语句执行成功,则自动进行提交(commit),如果语句执行失败,根据错误自动进行提交(commit)或回滚(rollback)。
在启用自动提交时,可以使用 START TRANSACTION
或 BEGIN
语句显示启动一个事务,使用 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 UPDATE
或 FOR SHARE
子句的 INSERT INTO ... SELECT
,UPDATE ... (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 SHARE 和 SELECT ... FOR UPDATE:如果使用唯一索引,只获取扫描行的锁。SELECT ... FOR UPDATE 会阻塞其他会话执行 SELECT ... FOR SHARE。
Locking Reads,UPDATE 以及 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 Levels 中 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 Files
Important Storage Considerations
- 在某些情况下,可以通过将数据文件和日志文件放在不同的物理磁盘上来提高数据库性能。还可以为 InnoDB 数据文件使用裸磁盘分区,提高 I/O 速度。
- 某些操作系统或磁盘子系统可能会延迟或重新排序写入操作以提高性能。操作系统崩溃或停电可能会破坏最近提交的数据,或者在最坏的情况下,会因为写入操作被重新排序而损坏数据库,可以通过断电来进行测试。在 Linux 环境,如果使用的是 ATA/SATA 磁盘,建议使用命令
hdparm -W0 /dev/hda
禁用回写缓存。需要注意的是,某些驱动器或磁盘控制器可能无法禁用回写缓存。 - InnoDB 默认启用双写缓冲区来保护用户数据,提高性能,参考:Doublewrite Buffer
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
autoextend
和 max
属性只能用于最后一个数据文件。
[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
InnoDB Doublewrite Buffer File Configuration
在 MySQL 8.0.20 之前,双写缓冲区位于 InnoDB 系统表空间中。从 MySQL 8.0.20 开始,双写缓冲区位于双写文件中。具体配置参考:Doublewrite Buffer
Redo Log Configuration
从 MySQL 8.0.30 开始,参数 innodb_redo_log_capacity
控制重做日志文件占用的磁盘空间。具体配置参考:Redo Log
Undo Tablespace Configuration
默认 UNDO 表空间创建在参数 innodb_undo_directory
指定的位置。 UNDO 日志的 I/O 模式使得 UNDO 表空间非常适合 SSD 存储。具体配置参考:Undo Tablespaces
Global Temporary Tablespace Configuration
全局临时表空间存储对用户创建的临时表所做更改的回滚段。具体配置参考:Global Temporary Tablespace
Session Temporary Tablespace Configuration
会话临时表空间存储用户创建的临时表和优化器创建的内部临时表。具体配置参考:Session Temporary Tablespaces
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 Buffer。
使用参数 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_size | 32G |
global_connection_memory_limit | 16G |
connection_memory_chunk_size | 8192 |
connection_memory_limit | 64M |
global_connection_memory_tracking | ON |
在上述配置中,设置了每个会话中普通用户执行的 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_code
和 Innodb_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_code
和 Innodb_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 Algorithm
使用参数 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_pct
和 innodb_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_ahead
为 ON
(默认为 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
- 启用
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%';
- 启用事件使用者表:
[(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)
- 启用参数
innodb_buffer_pool_dump_now
转储当前缓冲池状态:
[(none)]> SET GLOBAL innodb_buffer_pool_dump_now=ON;
Query OK, 0 rows affected (0.00 sec)
- 查看缓冲池转储状态,确认操作完成:
[(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)
- 启用参数
innodb_buffer_pool_load_now
加载缓冲池:
[(none)]> SET GLOBAL innodb_buffer_pool_load_now=ON;
Query OK, 0 rows affected (0.00 sec)
- 查询表 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 表示估计的剩余页数。
- 查询表 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 文件记录运行进程的状态和内存映像。所以当 mysqld 进程失效时,具有大缓冲池的系统可以生成大 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 variable | innodb_buffer_pool_in_core_file variable | madvise() MADV_DONTDUMP Support | Outcome |
---|---|---|---|
OFF (default) | Not relevant to outcome | Not relevant to outcome | Core file is not generated |
ON | ON (default) | Not relevant to outcome | Core file is generated with buffer pool pages |
ON | OFF | Yes | Core file is generated without buffer pool pages |
ON | OFF | No | Core file is not generated, core_file is disabled, and a warning is written to the server error log |
Core 文件大小还会受到 InnoDB 页大小的影响。较小的页大小意味着相同数量的数据需要更多的页,而更多的页意味着更多的页元数据。下表提供了不同页大小的 1G 缓冲池的示例:
innodb_page_size Setting | Buffer Pool Pages Included (innodb_buffer_pool_in_core_file=ON) | Buffer Pool Pages Excluded (innodb_buffer_pool_in_core_file=OFF) |
---|---|---|
4KB | 2.1GB | 0.9GB |
64KB | 1.7GB | 0.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_threads
和 innodb_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_threads
和 innodb_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_max
为 innodb_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_delay
和 innodb_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 TABLE
和ALTER TABLE
的STATS_PERSISTENT
,STATS_AUTO_RECALC
和STATS_SAMPLE_PAGES
子句为表配置优化器统计信息。 - 可以使用表
mysql.innodb_table_stats
和mysql.innodb_index_stats
查询优化器统计数据。 - 可以查询表
mysql.innodb_table_stats
和mysql.innodb_index_stats
的last_update
字段查看统计信息上次更新的时间。 - 可以修改表
mysql.innodb_table_stats
和mysql.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_stats
和 mysql.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_persistent
,innodb_stats_auto_recalc
和 innodb_stats_persistent_sample_pages
这几个全局变量的设置,单独为某个表配置优化器统计信息参数,可以使用 CREATE TABLE
和 ALTER TABLE
的 STATS_PERSISTENT
, STATS_AUTO_RECALC
和 STATS_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_stats
和 mysql.innodb_index_stats
。
innodb_table_stats
字段:
Column name | Description |
---|---|
database_name | Database name |
table_name | Table name, partition name, or subpartition name |
last_update | A timestamp indicating the last time that InnoDB updated this row |
n_rows | The number of rows in the table |
clustered_index_size | The size of the primary index, in pages |
sum_of_other_index_sizes | The total size of other (non-primary) indexes, in pages |
innodb_index_stats
字段:
Column name | Description |
---|---|
database_name | Database name |
table_name | Table name, partition name, or subpartition name |
index_name | Index name |
last_update | A timestamp indicating the last time the row was updated |
stat_name | The name of the statistic, whose value is reported in the stat_value column |
stat_value | The value of the statistic that is named in stat_name column |
sample_size | The number of pages sampled for the estimate provided in the stat_value column |
stat_description | Description of the statistic that is named in the stat_name column |
表 mysql.innodb_table_stats
和 mysql.innodb_index_stats
的 last_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_stats
和 mysql.innodb_index_stats
,以强制执行特定的查询优化计划或在不修改数据库的情况下测试替代计划。如果手动更新统计信息,需使用 FLUSH TABLE tbl_name
语句加载更新的统计信息。
主从同步环境,不会同步表 mysql.innodb_table_stats
和 mysql.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=PRIMARY
,stat_name=n_diff_pfx01
,stat_value
为 1,表示索引第一列(字段 a)只有 1 个不同的值。 - 当
index_name=PRIMARY
,stat_name=n_diff_pfx02
,stat_value
为 5,表示索引前两列(字段 a,b)有 5 个不同的值。
对于二级索引,n_diff_pfxNN
有 4 行,虽然此非唯一二级索引只包含 2 个字段(c,d),但 InnoDB 会将主键列(a,b)加在后面:
- 当
index_name=i1
,stat_name=n_diff_pfx01
,stat_value
为 1,表示索引第一列(字段 c)只有 1 个不同的值。 - 当
index_name=i1
,stat_name=n_diff_pfx02
,stat_value
为 2,表示索引前两列(字段 c,d)有 2 个不同的值。 - 当
index_name=i1
,stat_name=n_diff_pfx03
,stat_value
为 2,表示索引前三列(字段 c,d,a)有 2 个不同的值。 - 当
index_name=i1
,stat_name=n_diff_pfx04
,stat_value
为 5,表示索引前四列(字段 c,d,a,b)有 5 个不同的值。
对于唯一索引,n_diff_pfxNN
有 2 行:
- 当
index_name=i2uniq
,stat_name=n_diff_pfx01
,stat_value
为 2,表示索引第一列(字段 e)有 2 个不同的值。 - 当
index_name=i2uniq
,stat_name=n_diff_pfx02
,stat_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 STATUS
,SHOW INDEX
,或者在启用参数innodb_stats_on_metadata
(默认为 OFF)时查询表information_schema.TABLES
或information_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_pages
或innodb_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_size
和innodb_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 Memory | Buffer Pool Size |
---|---|
Less than 1GB | 128MB (the default value) |
1GB to 4GB | detected server memory * 0.5 |
Greater than 4GB | detected server memory * 0.75 |
innodb_redo_log_capacity
:根据服务器内存进行配置。
Detected Server Memory | Buffer Pool Size | Redo Log Capacity |
---|---|---|
Less than 1GB | Not configured | 100MB |
Less than 1GB | Less than 1GB | 100MB |
1GB to 2GB | Not applicable | 100MB |
2GB to 4GB | Not configured | 1GB |
2GB to 4GB | Any configured value | round(0.5 * detected server memory in GB) * 0.5 GB |
4GB to 10.66GB | Not applicable | round(0.75 * detected server memory in GB) * 0.5 GB |
10.66GB to 170.66GB | Not applicable | round(0.5625 * detected server memory in GB) * 0.5 GB |
Greater than 170.66GB | Not applicable | 128GB |
innodb_log_file_size
:根据缓冲池大小配置。
Buffer Pool Size | Log File Size |
---|---|
Less than 8GB | 512MB |
8GB to 128GB | 1024MB |
Greater than 128GB | 2048MB |
innodb_log_files_in_group
:根据缓冲池大小配置,最小值为 2,自动配置从 MySQL 8.0.14 加入。
Buffer Pool Size | Number of Log Files |
---|---|
Less than 8GB | round(buffer pool size ) |
8GB to 128GB | round(buffer pool size * 0.75) |
Greater than 128GB | 64 |
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 TABLE
或 ALTER 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_CMP
或INFORMATION_SCHEMA.INNODB_CMP_PER_INDEX
,对比COMPRESS_OPS
与COMPRESS_OPS_OK
,检查整体压缩操作中成功的比率。 - 如果成功比例较高,那么该表可能适合压缩。
- 如果失败比例较高,可以调整参数
innodb_compression_level
,innodb_compression_failure_threshold_pct
和innodb_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
时,告警如下:
Level | Code | Message |
---|---|---|
Warning | 1478 | InnoDB: ignoring KEY_BLOCK_SIZE=n unless ROW_FORMAT=COMPRESSED. |
当启用参数 innodb_strict_mode
,指定 KEY_BLOCK_SIZE
并使用其他 ROW_FORMAT
时,会报错,不会创建表。
CREATE TABLE
或者 ALTER TABLE
中 ROW_FORMAT
和 KEY_BLOCK_SIZE
选项如下:
Option | Usage Notes | Description |
---|---|---|
ROW_FORMAT=REDUNDANT | Storage format used prior to MySQL 5.0.3 | Less efficient than ROW_FORMAT=COMPACT ; for backward compatibility |
ROW_FORMAT=COMPACT | Default storage format since MySQL 5.0.3 | Stores 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=DYNAMIC | Store 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=COMPRESSED | Compresses the table and indexes using zlib | |
KEY_BLOCK_SIZE=n | Specifies 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_mode
, CREATE TABLE
或者 ALTER TABLE
的告警和错误:
Syntax | Warning or Error Condition | Resulting ROW_FORMAT , as shown in SHOW TABLE STATUS |
---|---|---|
ROW_FORMAT=REDUNDANT | None | REDUNDANT |
ROW_FORMAT=COMPACT | None | COMPACT |
ROW_FORMAT=COMPRESSED or ROW_FORMAT=DYNAMIC or KEY_BLOCK_SIZE is specified | Ignored for file-per-table tablespaces unless innodb_file_per_table 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 ignored | the specified row format, or the default row format |
ROW_FORMAT=COMPRESSED and valid KEY_BLOCK_SIZE are specified | None; KEY_BLOCK_SIZE specified is used | COMPRESSED |
KEY_BLOCK_SIZE is specified with REDUNDANT , COMPACT or DYNAMIC row format | KEY_BLOCK_SIZE is ignored | REDUNDANT , COMPACT or DYNAMIC |
ROW_FORMAT is not one of REDUNDANT , COMPACT , DYNAMIC or COMPRESSED | Ignored 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 TABLE
的 COMPRESSION
属性启用页压缩。支持的压缩算法包括 Zlib
和 LZ4
。
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 Format | Compact Storage Characteristics | Enhanced Variable-Length Column Storage | Large Index Key Prefix Support | Compression Support | Supported Tablespace Types |
---|---|---|---|---|---|
REDUNDANT | No | No | No | No | system, file-per-table, general |
COMPACT | Yes | No | No | No | system, file-per-table, general |
DYNAMIC | Yes | Yes | Yes | No | system, file-per-table, general |
COMPRESSED | Yes | Yes | Yes | Yes | file-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 TABLE
或 ALTER TABLE
语句,或者在 CREATE TABLE
或 ALTER TABLE
语句中使用 TABLESPACE [=] innodb_system
选项。
DYNAMIC 行格式是 COMPACT 行格式的变体。存储特性参考:COMPACT Row Format
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 Format
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 TABLE
或 ALTER 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)
Doublewrite Buffer
双写缓冲区是一个存储区域,InnoDB 在其中写入从缓冲池刷新的页,然后将页写入 InnoDB 数据文件中。如果在页写入过程中出现操作系统、存储系统或意外的 mysqld 进程退出异常,InnoDB 可以在崩溃恢复期间从双写缓冲区中找到页的正确副本。
具体参考:Doublewrite Buffer
File Space Management
在参数文件中使用参数 innodb_data_file_path
指定 InnoDB 系统表空间数据文件。具体参数:The System Tablespace
默认启用参数 innodb_file_per_table
将创建的表存储在独立表空间。具体参考:File-Per-Table Tablespaces
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 Formats
- DYNAMIC 和 COMPRESSED 行格式:当选择可变长度列进行页外(Off-Page)存储时,InnoDB 在行中本地存储一个 20 字节的指针。参考:InnoDB Row Formats
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 factor)可能在 50% 到 100% 之间变化。碎片化的另一个现象是,像对下面表扫描所花费的时间比 “应该” 花费的时间更长:
SELECT COUNT(*) FROM t WHERE non_indexed_column <> 12345;
为加快索引扫描,可以定期执行空的 ALTER TABLE
操作,重新生成表:
ALTER TABLE tbl_name ENGINE=INNODB;
ALTER TABLE tbl_name FORCE;
执行碎片整理操作的另一种方法是使用 mysqldump 将表转储到文本文件,删除表,然后从转储文件中重新加载。 如果索引的插入总是升序,并且只从末尾删除记录,那么 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
语句的 ALGORITHM
和 LOCK
子句来控制 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=INSTANT
、ALGORITHM=INPLACE
和ALGORITHM=COPY
的语句。也可以启用参数old_alter_table
强制使用ALGORITHM=COPY
。
Online DDL Operations
Index Operations
对于索引的在线 DDL 操作如下表:
Operation | Instant | In Place | Rebuilds Table | Permits Concurrent DML | Only Modifies Metadata |
---|---|---|---|---|---|
Creating or adding a secondary index | No | Yes | No | Yes | No |
Dropping an index | No | Yes | No | Yes | Yes |
Renaming an index | No | Yes | No | Yes | Yes |
Adding a FULLTEXT index | No | Yes* | No* | No | No |
Adding a SPATIAL index | No | Yes | No | No | No |
Changing the index type | Yes | Yes | No | Yes | Yes |
- 创建或增加二级索引
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 操作如下表:
Operation | Instant | In Place | Rebuilds Table | Permits Concurrent DML | Only Modifies Metadata |
---|---|---|---|---|---|
Adding a primary key | No | Yes* | Yes* | Yes | No |
Dropping a primary key | No | No | Yes | No | No |
Dropping a primary key and adding another | No | Yes | Yes | Yes | No |
- 增加主键
ALTER TABLE tbl_name ADD PRIMARY KEY (column), ALGORITHM=INPLACE, LOCK=NONE;
重组聚集索引总是需要复制表数据。因此,最好在创建表时定义主键。MySQL 通过将现有数据从原始表复制到具有所需索引结构的临时表来创建新的聚集索引。一旦数据被完全复制到临时表中,原始表就会用不同的临时表名重命名。使用原始表的名称重命名包含新聚集索引的临时表,并将原始表从数据库中删除。
尽管仍然需要复制数据,使用 ALGORITHM=INPLACE
比 ALGORITHM=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 操作如下表:
Operation | Instant | In Place | Rebuilds Table | Permits Concurrent DML | Only Modifies Metadata |
---|---|---|---|---|---|
Adding a column | Yes* | Yes | No* | Yes* | Yes |
Dropping a column | Yes* | Yes | Yes | Yes | Yes |
Renaming a column | Yes* | Yes | No | Yes* | Yes |
Reordering columns | No | Yes | Yes | Yes | No |
Setting a column default value | Yes | Yes | No | Yes | Yes |
Changing the column data type | No | No | Yes | No | No |
Extending VARCHAR column size | No | Yes | No | Yes | Yes |
Dropping the column default value | Yes | Yes | No | Yes | Yes |
Changing the auto-increment value | No | Yes | No | Yes | No* |
Making a column NULL | No | Yes | Yes* | Yes | No |
Making a column NOT NULL | No | Yes* | Yes* | Yes | No |
Modifying the definition of an ENUM or SET column | Yes | Yes | No | Yes | Yes |
- 增加列
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 TABLE
或 OPTIMIZE TABLE
操作重建具有即时(instant)添加或删除列的表时, TOTAL_ROW_VERSIONS
值将被重置为 0。允许的最大行版本数为 64,当达到此限制时,使用 ALGORITHM=INSTANT
的 ADD COLUMN
和 DROP 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=INPLACE
或 ALGORITHM=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;
就地重建表。昂贵操作。
- 修改
ENUM
或SET
列定义
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 操作如下表:
Operation | Instant | In Place | Rebuilds Table | Permits Concurrent DML | Only Modifies Metadata |
---|---|---|---|---|---|
Adding a foreign key constraint | No | Yes* | No | Yes | Yes |
Dropping a foreign key constraint | No | Yes | No | Yes | Yes |
- 增加外键约束
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 操作如下表:
Operation | Instant | In Place | Rebuilds Table | Permits Concurrent DML | Only Modifies Metadata |
---|---|---|---|---|---|
Changing the ROW_FORMAT | No | Yes | Yes | Yes | No |
Changing the KEY_BLOCK_SIZE | No | Yes | Yes | Yes | No |
Setting persistent table statistics | No | Yes | No | Yes | Yes |
Specifying a character set | No | Yes | Yes* | Yes | No |
Converting a character set | No | No | Yes* | No | No |
Optimizing a table | No | Yes* | Yes | Yes | No |
Rebuilding with the FORCE option | No | Yes* | Yes | Yes | No |
Performing a null rebuild | No | Yes* | Yes | Yes | No |
Renaming a table | Yes | Yes | No | Yes | Yes |
- 修改
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
算法,但不允许使用 ALGORITHM
和 LOCK
语法。
- 使用
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 Clause | Instant | In Place | Permits DML | Notes |
---|---|---|---|---|
PARTITION BY | No | No | No | Permits ALGORITHM=COPY , `LOCK={DEFAULT |
ADD PARTITION | No | Yes* | Yes* | `ALGORITHM=INPLACE, LOCK={DEFAULT |
DROP PARTITION | No | Yes* | Yes* | `ALGORITHM=INPLACE, LOCK={DEFAULT |
DISCARD PARTITION | No | No | No | Only permits ALGORITHM=DEFAULT , LOCK=DEFAULT |
IMPORT PARTITION | No | No | No | Only permits ALGORITHM=DEFAULT , LOCK=DEFAULT |
TRUNCATE PARTITION | No | Yes | Yes | Does 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 PARTITION | No | Yes* | No | `ALGORITHM=INPLACE, LOCK={DEFAULT |
REORGANIZE PARTITION | No | Yes* | No | `ALGORITHM=INPLACE, LOCK={DEFAULT |
EXCHANGE PARTITION | No | Yes | Yes | |
ANALYZE PARTITION | No | Yes | Yes | |
CHECK PARTITION | No | Yes | Yes | |
OPTIMIZE PARTITION | No | No | No | ALGORITHM and LOCK clauses are ignored. Rebuilds the entire table. |
REBUILD PARTITION | No | Yes* | No | `ALGORITHM=INPLACE, LOCK={DEFAULT |
REPAIR PARTITION | No | Yes | Yes | |
REMOVE PARTITIONING | No | No | No | Permits 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:初始化。在初始化阶段,服务器将考虑存储引擎功能、语句中指定的操作以及用户指定的
ALGORITHM
和LOCK
选项,确定操作过程中允许的并发量。在此阶段,将使用共享的可升级元数据锁来保护当前表定义。 - 阶段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=INSTANT
、 ALGORITHM=INPLACE
和 ALGORITHM=COPY
的语句。也可以启用参数 old_alter_table
强制使用 ALGORITHM=COPY
。
对于修改表数据的 DDL 操作,可以通过查看命令完成后显示的 “rows affected” 值来确定 DDL 操作是就地执行更改还是执行表复制。
在对大表运行 DDL 操作之前,进行如下测试步骤:
- 克隆表结构。
- 插入少量数据到克隆表。
- 对克隆表运行 DDL 操作。
- 检查 “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 操作不兼容。- 在等待表上的排他锁时发生超时。
tmpdir
或innodb_tmpdir
空间不足。- 操作花费时间过长,并且并发 DML 对表的修改过大,以至于临时日志文件的大小超过
innodb_online_alter_log_max_size
,则在线 DDL 操作将失败,报 DB_ONLINE_LOG_TOO_BIG 错误。 - 在线 DDL 操作期间的并发 DML 适用于修改前的表,不适用于修改后的表。例如在创建唯一索引期间插入了重复值。
Online DDL Limitations
在线 DDL 的限制有:
- 在临时表上创建索引时会复制该表。
- 如果表上存在
ON...CASCADE
或ON...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 id | waiting thread | waiting query | blocking trx id | blocking thread | blocking query |
---|---|---|---|---|---|
A4 | 6 | SELECT b FROM t FOR UPDATE | A3 | 5 | SELECT SLEEP(100) |
A5 | 7 | SELECT c FROM t FOR UPDATE | A3 | 5 | SELECT SLEEP(100) |
A5 | 7 | SELECT c FROM t FOR UPDATE | A4 | 6 | SELECT 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 id | trx state | trx started | trx requested lock id | trx wait started | trx weight | trx mysql thread id | trx query |
---|---|---|---|---|---|---|---|
A3 | RUNNING | 2008-01-15 16:44:54 | NULL | NULL | 2 | 5 | SELECT SLEEP(100) |
A4 | LOCK WAIT | 2008-01-15 16:45:09 | A4:1:3:2 | 2008-01-15 16:45:09 | 2 | 6 | SELECT b FROM t FOR UPDATE |
A5 | LOCK WAIT | 2008-01-15 16:45:14 | A5:1:3:2 | 2008-01-15 16:45:14 | 2 | 7 | SELECT c FROM t FOR UPDATE |
表 data_locks 输出示例:
lock id | lock trx id | lock mode | lock type | lock schema | lock table | lock index | lock data |
---|---|---|---|---|---|---|---|
A3:1:3:2 | A3 | X | RECORD | test | t | PRIMARY | 0x0200 |
A4:1:3:2 | A4 | X | RECORD | test | t | PRIMARY | 0x0200 |
A5:1:3:2 | A5 | X | RECORD | test | t | PRIMARY | 0x0200 |
表 data_lock_waits 输出示例:
requesting trx id | requested lock id | blocking trx id | blocking lock id |
---|---|---|---|
A4 | A4:1:3:2 | A3 | A3:1:3:2 |
A5 | A5:1:3:2 | A3 | A3:1:3:2 |
A5 | A5:1:3:2 | A4 | A4:1:3:2 |
Identifying a Blocking Query After the Issuing Session Becomes Idle
在定位阻塞事务时,如果发出查询的会话空闲,则会返回 NULL 值。在这种情况下,使用以下步骤来确定阻塞查询:
- 确认阻止事务的进程 ID。为表
sys.innodb_lock_waits
的blocking_pid
字段值。 - 使用
blocking_pid
查询表performance_schema.threads
获取阻塞事务的THREAD_ID
。例如:
SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_ID = 6;
- 使用
THREAD_ID
查询表performance_schema.events_statements_current
获取最后执行的 SQL 语句。例如:
SELECT THREAD_ID, SQL_TEXT FROM performance_schema.events_statements_current
WHERE THREAD_ID = 28\G
- 如果线程最后执行的语句不足以定位阻塞原因,查询表
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 表外键列元数据。
例子:表,列,索引,表空间元数据
- 创建测试表:
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);
- 查询表信息:
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)
- 查询列信息:
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。
- 查询索引信息
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。
- 查询索引字段信息
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FIELDS where INDEX_ID = 112 \G
*************************** 1. row ***************************
INDEX_ID: 112
NAME: col1
POS: 0
- 查询表所在的表空间信息
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
- 查询表空间对应的数据文件
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_DATAFILES WHERE SPACE = 57 \G
*************************** 1. row ***************************
SPACE: 57
PATH: ./test/t1.ibd
- 插入数据并查询表统计信息
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
例子:外键元数据
- 创建数据库及父子表
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;
- 查询外键信息
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
- 查询外键列信息
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
)
例子:启用,禁用,重置,查询计数器
- 创建表
mysql> USE test;
Database changed
mysql> CREATE TABLE t1 (c1 INT) ENGINE=INNODB;
Query OK, 0 rows affected (0.02 sec)
- 启用
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 |
+-------------+-------------------------+
- 查询
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
- 插入数据
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)
- 再次查询
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
- 重置
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
- 重置所有计数器值,需要先禁用计数器
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
- 禁用计数器后,重置所有计数器
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
特性
- 创建 InnoDB 临时表
mysql> CREATE TEMPORARY TABLE t1 (c1 INT PRIMARY KEY) ENGINE=INNODB;
- 查询
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)。
- 重启 MySQL 后再次查询
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO\G
返回空。
- 创建临时表
mysql> CREATE TEMPORARY TABLE t1 (c1 INT PRIMARY KEY) ENGINE=INNODB;
- 查询
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
- 启用
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
- 启用
events_stages_current
,events_stages_history
和events_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
- 执行
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
- 查看
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,具体步骤如下:
- 查看可用的 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 |
+---------------------------------------------------------+---------+-------+
- 在配置文件中启用上面的 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)
- 修改表
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_current
,events_waits_history
和 events_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)
- 启用了 Instruments 和 Consumers 后,运行需要监控的负载,例如使用
mysqlslap
模拟负载。
$> ./mysqlslap --auto-generate-sql --concurrency=100 --iterations=10
--number-of-queries=1000 --number-char-cols=6 --number-int-cols=6;
- 查询等待事件数据,例如查询表
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 秒将输出写入 mysqld 服务器标准错误输出(stderr)。
InnoDB 将监视器输出发送到 stderr,而不是 stdout 或固定大小的内存缓冲区,以避免潜在的缓冲区溢出。
在 Windows 上,除非另有配置,否则 stderr 将被定向到默认日志文件。如果要将输出定向到控制台窗口而不是错误日志,使用 --console
选项从控制台窗口中的命令提示符启动 MySQL。
在 Unix 和类 Unix 系统上,除非另有配置,否则 stderr 通常被定向到终端。参考 Error Log
由于生成监视器输出会影响性能,故仅当实际需要查看监视器信息时,才应启用 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_output
为 ON
启用标准 InnoDB 监视器:
SET GLOBAL innodb_status_output=ON;
设置参数 innodb_status_output
为 OFF
禁用标准 InnoDB 监视器:
SET GLOBAL innodb_status_output=OFF;
关闭 MySQL Server,会将参数 innodb_status_output
设置为默认 OFF
。
Enabling the InnoDB Lock Monitor
要输出 InnoDB 锁监视器数据,需求启用标准 InnoDB 监视器和 InnoDB 锁监视器。
设置参数 innodb_status_output_locks
为 ON
启用 InnoDB 锁监视器:
SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;
设置参数 innodb_status_output_locks
为 OFF
禁用 InnoDB 锁监视器:
SET GLOBAL innodb_status_output_locks=OFF;
关闭 MySQL Server,会将参数 innodb_status_output_locks
设置为默认 OFF
。
注意:
如果启用 InnoDB 锁监视器用于
SHOW ENGINE INNODB STATUS
输出,则只需要设置参数innodb_status_output_locks
为ON
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 Locking。
TRANSACTIONS:如果显示锁等待,则可能存在锁争用。输出还有助于跟踪事务死锁的原因。
FILE I/O:显示有关 InnoDB 用于执行各种类型 I/O 的线程信息。其中前几个专用于一般的 InnoDB 处理。还显示挂起的 I/O 操作信息和 I/O 性能的统计信息。线程数量由参数
innodb_read_io_threads
和innodb_write_io_threads
指定,参考 Configuring the Number of Background InnoDB I/O Threads。INSERT BUFFER AND ADAPTIVE HASH INDEX:显示 Change Buffer 和 Adaptive Hash Index 状态。
LOG:显示有关 InnoDB 重做日志信息。内容包括当前日志序列号、刷新到磁盘的日志位置以及 InnoDB 上次执行检查点的位置。
BUFFER POOL AND MEMORY:显示有关读取页和写入页的统计信息,可以计算出当前查询涉及的数据文件 I/O 操作数。参考 Monitoring the Buffer Pool Using the InnoDB Standard Monitor 和 Buffer Pool。
ROW OPERATIONS:显示主线程正在执行的操作,包括每种行操作类型的数量和速率。
InnoDB Troubleshooting
对于 InnoDB 的故障排除,参考以下准则:
- 当出现错误或者 Bug 时,查看错误日志( Error Log),具体错误信息参考 Server Error Message Reference。
- 如果是死锁问题,启用参数
innodb_print_all_deadlocks
以便将死锁详细信息输出到错误日志,参考 Deadlocks in InnoDB。 - 如果是 InnoDB 数据字典问题,参考 Troubleshooting InnoDB Data Dictionary Operations。
- 启用 InnoDB 监视器获取问题信息,参考 InnoDB Monitors。如果是性能问题,或者 MySQL 挂起,需要启用标准监视器;如果是锁问题,需要启用锁监视器。如果是表创建,表空间或者数据字典操作问题,参考 InnoDB INFORMATION_SCHEMA Schema Object Tables 检查 InnoDB 内部数据字典内容。InnoDB 会在以下几种情况临时启用标准 InnoDB 监视器输出:
- 长时间信号量等待
- 在缓冲池中找不到空闲块
- 锁堆或自适应哈希索引占用超过 67% 的缓冲池
- 如果怀疑表损坏,对表执行
CHECK TABLE
命令。
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 日志目录存在。
确保 mysqld 具有在这些目录中创建文件的权限。
确保 mysqld 可以读取正确的
my.cnf
参数文件。确保足够的磁盘空间和磁盘配额。
确保为子目录和数据文件指定的名称不冲突。
仔细检查
innodb_data_home_dir
和innodb_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
失败导致的失控回滚,可以杀掉 mysqld 进程并将 innodb_force_recovery
设置为 3 以在不回滚的情况下启动数据库,然后删除导致失控回滚的表。
如果表数据中的损坏阻止导出整个表,则具有 ORDER BY primary_key DESC
子句的查询可能能够导出损坏部分之后的表数据。
如果需要大的 innodb_force_recovery
值才能启动 InnoDB,则可能存在损坏的数据结构,可能会导致复杂查询(包含 WHERE
、ORDER BY
或其他子句的查询)失败。在这种情况下,可能只能运行基本的 SELECT * FROM t
查询。
Troubleshooting InnoDB Data Dictionary Operations
表定义信息存储在 InnoDB 数据字典中。如果移动数据文件,则字典数据会变得不一致。
如果数据字典损坏或一致性问题阻止 InnoDB 启动,参考 Forcing InnoDB Recovery 获取有关手动恢复的信息。
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 Management。 - 表所有字段总长度限制为 65535 字节。
- 重做日志总大小不超过 512 GB。
- 表空间最小为 10 MB,最大如下表:
InnoDB Page Size | Maximum Tablespace Size |
---|---|
4KB | 16TB |
8KB | 32TB |
16KB | 64TB |
32KB | 128TB |
64KB | 256TB |
支持 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 实例不能使用其他使用不同页面大小的实例中的数据文件或日志文件。