MySQL 8.0 20个 InnoDB 及数据字典相关的新特性

时间:2023-02-20 14:08:45

本篇将聚焦于 MySQL 8.0 InnoDB 及数据字典相关的新特性。这些新特性涉及的范围包括:Redo log、事务调度算法、自增主键、回滚表空间、死锁、临时表空间、Doublewrite Buffer、数据字典、原子 DDL、information_schema、备份等。

1. Redo log 的优化

相关的优化有:

  • 对 redo log 进行了无锁设计
  • 允许多个用户线程并发写入 redo log buffer
  • 可动态修改 innodb_log_buffer_size 的大小

2. 事务调度算法

当有多个事务竞争同一把锁时,在 MySQL 8.0 之前,使用的是 FIFO(First In First Out,先进先出)算法,该算法会将锁优先分配给最先请求的事务。

在 MySQL 8.0 中,引入了 CATS (Contention-Aware Transaction Scheduling,竞争感知事务调度)算法,该算法会计算每个事务阻塞的事务数,然后将锁优先分配给阻塞事务最多的事务。

3. 自增主键的持久化

在 MySQL 8.0 之前,自增主键的分配是由 InnoDB 数据字典内部一个计数器来决定的。该计数器只在内存中维护,并不会持久化到磁盘中。

当数据库重启后,会基于自增主键的最大值(假设是 max_id)来重新初始化自增主键。

所以如果 max_id 之后的自增主键值分配但又删除了,则数据库重启后,会继续从 max_id + 1 开始分配自增主键值。

这就意味着,之前分配的自增主键值(虽然被删除了)又被重新分配了。

当然,大多数情况下也没有问题,不过在以下两种场景需要注意:

  • 业务将自增主键作为业务主键。在使用的过程中可能会发现两笔不同的订单对应着同一个业务主键。

  • 归档场景。在归档的过程中有可能会出现主键冲突。

具体细节可参考:MySQL 8 新特性之自增主键的持久化

4. 默认开启回滚表空间

在 MySQL 5.7 中,因 innodb_undo_tablespaces 默认为 0,所以回滚表空间默认是没有开启的。若要开启,只能在初始化时将 innodb_undo_tablespaces 设置为非 0 值。

在 MySQL 8.0 中,innodb_undo_tablespaces 默认为 2。不仅如此,innodb_undo_tablespaces 还可在线动态修改。

从 MySQL 8.0.14 开始,支持通过 SQL 语句来管理回滚表空间。

# 添加回滚表空间,文件扩展名必须是.ibu。
CREATE UNDO TABLESPACE tablespace_name ADD DATAFILE 'file_name.ibu';
# 删除回滚表空间。
DROP UNDO TABLESPACE tablespace_name;
# 在删除回滚表空间之前,必须先将它设置为 INACTIVE 状态。
ALTER UNDO TABLESPACE tablespace_name SET INACTIVE;
# 将回滚表空间设置为 ACTIVE 状态。
ALTER UNDO TABLESPACE tablespace_name SET ACTIVE;

与此同时,innodb_undo_tablespaces 参数被弃用,虽然还是可以修改,但不起作用,innodb_undo_tablespaces 永远为 2。

5. TempTable 引擎

默认的内存临时表由 MEMORY 引擎更改为 TempTable 引擎,相比于前者,后者支持以变长方式存储 VARCHAR、VARBINARY 等变长字段。

从 MySQL 8.0.13 开始,TempTable 引擎支持 BLOB 字段。

6. innodb_dedicated_server

引入了 innodb_dedicated_server 参数,可基于服务器的内存动态设置 innodb_buffer_pool_size、innodb_redo_log_capacity 和 innodb_flush_method。

7. 关闭死锁检测

引入了 innodb_deadlock_detect 参数关闭死锁检测。

8. 在线回收临时表空间的磁盘空间

在 MySQL 5.7 中,用户创建的临时表和磁盘临时表会存储在全局临时表空间(ibtmp1)中。

ibtmp1 一旦增长,就不会收缩。如果要回收 ibtmp1 的空间,就只能重启实例。

在 MySQL 8.0 中,用户创建的临时表和磁盘临时表会存储在会话临时表空间中。会话临时表空间默认位于 #innodb_temp 目录下。

一个会话最多会分配两个临时表空间,分别用来存储用户临时表和磁盘临时表。当会话的连接断开时,会清空这两个临时表空间。

9. 设置独立的双写缓冲区(Doublewrite Buffer)

在 MySQL 8.0.20 之前,双写缓冲区默认存储在 InnoDB 系统表空间(ibdata1)中。

从 MySQL 8.0.20 开始,双写缓冲区会存储在单独的 #ib_16384_0.dblwr 和 #ib_16384_1.dblwr 文件中。

从 MySQL 8.0.30 开始,innodb_doublewrite 在 ON(开启双写缓冲区) 和 OFF(关闭双写缓冲区) 的基础上新增了两个选项:

  • DETECT_AND_RECOVER:作用与 ON 一样(实际上是 ON 的同义词)。

    在进行脏页刷新时,会首先将数据页写到双写缓冲区中。在进行故障恢复时,会通过双写缓冲区来修复数据文件中不完整的数据页。

  • DETECT_ONLY:也会开启双写缓冲区,只不过在进行脏页刷新时,只会记录数据页的元数据信息,不会存储数据页的内容。

    所以在进行故障恢复时,即使数据文件中存在不完整的数据页,也不能通过双写缓冲区来修复。

    该选项主要是用来检测实例启动时是否有不完整的数据页。

10. 禁用重做日志记录(redo logging)

在 MySQL 8.0.21 开始,可以使用 ALTER INSTANCE DISABLE INNODB REDO_LOG 命令禁用重做日志记录。

该特性适用于数据加载场景,线上生产环境切记不要禁用。

禁用后,可通过 ALTER INSTANCE ENABLE INNODB REDO_LOG 命令开启。

11. 查看表缓存在 Buffer Pool 中数据页的数量

可通过 information_schema.innodb_cached_indexes 查看表缓存在 Buffer Pool 中数据页的数量。

SELECT 
  tables.name, indexes.name, cached.n_cached_pages
FROM
  information_schema.innodb_cached_indexes AS cached,
  information_schema.innodb_indexes AS indexes,
  information_schema.innodb_tables AS tables
WHERE
  cached.index_id = indexes.index_id AND indexes.table_id = tables.table_id;
+------------+------------+----------------+
| table_name | index_name | n_cached_pages |
+------------+------------+----------------+
| db1/t1     | idx_c1     |            279 |
| db1/t1     | PRIMARY    |           4756 |
+------------+------------+----------------+
2 rows in set (0.00 sec) 

12. 动态调整 redo log 的容量

从 MySQL 8.0.30 开始,可通过 innodb_redo_log_capacity 参数来动态调整 redo log 的容量。

在 MySQL 8.0.30 之前,InnoDB 默认会在数据目录下创建两个 redo log。Redo log 的数量和大小分别由 innodb_log_files_in_group 和 innodb_log_file_size 决定。

随着 innodb_redo_log_capacity 的引入,InnoDB 会在数据目录的 #innodb_redo 目录下创建 32 个 redo log。Redo log 的总大小由 innodb_redo_log_capacity 决定。

innodb_redo_log_capacity 如果没有显式设置,则默认等于 innodb_log_file_size * innodb_log_files_in_group。

13. 基于 InnoDB 的数据字典

引入了原生的、基于 InnoDB 的数据字典。这些数据字典表位于 mysql 库中,对用户不可见。

如果是 debug 版本,可通过以下方式访问数据字典表。

mysql> SET SESSION debug='+d,skip_dd_table_access_check';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT name, schema_id, hidden, type FROM mysql.tables WHERE schema_id=1 AND hidden='System';
+------------------------------+-----------+--------+------------+
| name                         | schema_id | hidden | type       |
+------------------------------+-----------+--------+------------+
| catalogs                     |         1 | System | BASE TABLE |
| character_sets               |         1 | System | BASE TABLE |
| check_constraints            |         1 | System | BASE TABLE |
| collations                   |         1 | System | BASE TABLE |
...
+------------------------------+-----------+--------+------------+
32 rows in set (0.02 sec)

在 MySQL 8.0 中,mysql 库中的 InnoDB 表(包括数据字典表)会存储在数据目录下的 mysql.ibd 文件中,而其它非 InnoDB 表则依旧存储在 mysql 目录下。

[root@mysql data]# ls mysql
general_log_213.sdi  general_log.CSM  general_log.CSV  slow_log_214.sdi  slow_log.CSM  slow_log.CSV

14. 移除了 frm 等文件

移除了之前版本的 frm、par、TRN、TRG、isl、db.opt、ddl_log.log 等文件,这些文件会存储部分元数据信息。

15. 不再识别手动创建的数据库目录

因为数据字典的引入,MySQL 不再识别手动创建的数据库目录。

看下面这个示例。

在 MySQL 5.7 实例的数据目录下创建一个目录,该目录会被 MySQL 识别。

# cd /data/mysql/3307/data/
# mkdir test

mysql5.7> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| test               |
+--------------------+
5 rows in set (0.00 sec)

mysql5.7USE test
Database changed
mysql5.7SHOW TABLES;
Empty set (0.00 sec)

相同的操作如果是在 MySQL 8.0 中执行,则不会识别。

16. 原子 DDL

在 MySQL 8.0 之前,元数据信息会存储在多个文件中。在进行 DDL 的过程中,如果实例异常重启,可能会导致这些文件中的元数据信息不一致,甚至产生 Orphan(孤儿)表。

MySQL 8.0 引入了原子 DDL,可将 DDL 操作相关的元数据更新、存储引擎操作和 binlog 写入放到一个原子操作中。

这样就能保证 DDL 操作要么提交,要么回滚,不存在任何中间状态,即使 DDL 期间实例发生了重启。

除此之外,一些 DDL 操作的行为也发生了变化,看下面这个示例。

# MySQL 5.7
mysql5.7> CREATE TABLE t1(id INT);
Query OK, 0 rows affected (0.02 sec)

# 虽然命令报错了,但 t1 表还是被删除了
mysql5.7> DROP TABLE t1,t2;
ERROR 1051 (42S02): Unknown table 'slowtech.t2'

mysql5.7> SHOW TABLES;
Empty set (0.00 sec)

# MySQL 8.0
mysql8.0CREATE TABLE t1(id INT);
Query OK, 0 rows affected (0.03 sec)

# 命令报错,t1 表没被删除
mysql8.0> DROP TABLE t1,t2;
ERROR 1051 (42S02): Unknown table 'slowtech.t2'

mysql8.0> SHOW TABLES;
+--------------------+
| Tables_in_slowtech |
+--------------------+
| t1                 |
+--------------------+
1 row in set (0.00 sec)

17. SDI

数据库对象的元数据信息除了存储在数据字典中,也会再存储一份 SDI(Serialized Dictionary Information,序列化的字典信息)。

当数据字典不可用时,我们可以基于 SDI 提取对象的元数据信息。

对于 InnoDB 表,SDI 会存储在 .ibd 文件中,需通过 ibd2sdi 命令查看。

对于其它存储引擎的表,SDI 会存储在 .sdi 文件中,可直接通过 cat 命令查看。

18. information_schema 查询性能提升

information_schema 中的部分表已重构为基于数据字典的视图,在此之前,这些表是通过临时表来实现的。

information_schema.statistics 和 information_schema.tables 两张表中表相关的统计信息会缓存起来以提升查询性能。

缓存的时间由 information_schema_stats_expiry 参数决定,默认是 86400 秒(24小时)。

如果要更新某张表的统计信息,可执行 ANALYZE TABLE 操作。

19. information_schema 表中的列名会以大写形式返回

查询时,在没有指定别名的情况下,返回的列名会与查询指定的列名一致,包括大小写也一样。

看下面这个示例。

mysql> SELECT id FROM test.t1;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

mysql> SELECT Id FROM test.t1;
+----+
| Id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

在 MySQL 8.0 中,如果查询的是 information_schema 中的表,则列名会以大写形式返回。

# 无论查询使用的是 table_name 还是 Table_Name,返回的列名都是 TABLE_NAME。 
mysql8.0> SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'users';
+------------+
| TABLE_NAME |
+------------+
| users      |
+------------+
1 row in set (0.00 sec)

mysql8.0SELECT Table_Name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'users';
+------------+
| TABLE_NAME |
+------------+
| users      |
+------------+
1 row in set (0.00 sec)

20. 备份

在 MySQL 8.0 之前,在使用 mysqldump 或 mysqlpump 进行全库备份(--all-databases)时,即使没有指定 --routines 和 --events,也会备份存储过程(包括自定义函数)和定时器。

之所以会这样,是因为这两类对象的定义信息是存储在 mysql.proc 和 mysql.event 表中。当进行全库备份时,会备份 mysql 库中的所有表,自然也包括 mysql.proc 和 mysql.event。

在 MySQL 8.0 中,没有 mysql.proc 和 mysql.event 表,取而代之的是 mysql.routines 和 mysql.events。注意,替换后的表是数据字典表,对用户是不可见的。

所以,在 MySQL 8.0 中,如果要备份存储过程和定时器,需显式指定 --routines 和 --events。