本篇将聚焦于 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.7> USE test
Database changed
mysql5.7> SHOW 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.0> CREATE 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.0> SELECT 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。