众所周知,由于MySQL采用统一Server层+不同的底层引擎插件的架构模式,在Server层为每个表创建了frm文件,以保存与表定义相关的元数据信息。然而某些引擎(例如InnoDB)本身也会存储元数据,这样不仅产生了元数据冗余,而且由于Server层和引擎层分别各自管理,在执行DDL之类的操作时,很难做到crash-safe,更别说让DDL具备事务性了。
为了解决这些问题(尤其是DDL无法做到atomic),从MySQL8.0开始取消了FRM文件及其他server层的元数据文件(frm, par, trn, trg, isl,db.opt),所有的元数据都用InnoDB引擎进行存储, 另外一些诸如权限表之类的系统表也改用InnoDB引擎。
本文是笔者初次了解这块内容,因此不会过多深入,由于涉及的改动太多,后面有空再逐个展开。
本文所有测试和代码相关部分都是基于MySQL8.0.0版本,由于这是8.0大版本的第一个开发版本,不排除未来行为会发生变化。
测试
首先我们创建一个新库,并在库下创建两个表来开启我们的测试
mysql> CREATE DATABASE sbtest;
Query OK, 1 row affected (0.00 sec)
mysql> USE sbtest
Database changed
mysql> CREATE TABLE t1 (a int primary key);
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE t2 (a int primary key, b int);
Query OK, 0 rows affected (0.00 sec)
$ls -lh /u01/my80/data/sbtest
total 256K
-rw-r----- 1 yinfeng.zwx users 128K Oct 5 19:44 t1.ibd
-rw-r----- 1 yinfeng.zwx users 128K Oct 5 19:44 t2.ibd
$ls /u01/my80/data/sbtest_9.SDI
/u01/my80/data/sbtest_9.SDI
$cat /u01/my80/data/sbtest_9.SDI
{
"sdi_version": 1,
"dd_version": 1,
"dd_object_type": "Schema",
"dd_object": {
"name": "sbtest",
"default_collation_id": 33,
"created": 0,
"last_altered": 0
}
}
可以看到在库目录下只有ibd文件,并没有frm文件,而在数据目录下,相应的生成了一个SDI文件,来描述这个sbtest库的信息。
我们再来看看创建一个MYISAM引擎的表:
mysql> create database my;
Query OK, 1 row affected (0.00 sec)
mysql> use my
Database changed
mysql> create table t1 (a int, b varchar(320)) engine=myisam;
Query OK, 0 rows affected (0.00 sec)
$ls my/
t1_435.SDI t1.MYD t1.MYI
{
"sdi_version": 1,
"dd_version": 1,
"dd_object_type": "Table",
"dd_object": {
"name": "t1",
"mysql_version_id": 80000,
"created": 20161005201935,
"last_altered": 20161005201935,
"options": "avg_row_length=0;key_block_size=0;keys_disabled=0;pack_record=1;stats_auto_recalc=0;stats_sample_pages=0;",
"columns": [
{
"name": "a",
"type": 4,
"is_nullable": true,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": false,
"ordinal_position": 1,
"char_length": 11,
"numeric_precision": 10,
"numeric_scale": 0,
"datetime_precision": 0,
"has_no_default": false,
"default_value_null": true,
"default_value": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "interval_count=0;",
"se_private_data": "",
"column_key": 1,
"column_type_utf8": "int(11)",
"elements": [],
"collation_id": 33
},
{
"name": "b",
"type": 16,
"is_nullable": true,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": false,
"ordinal_position": 2,
"char_length": 960,
"numeric_precision": 0,
"numeric_scale": 0,
"datetime_precision": 0,
"has_no_default": false,
"default_value_null": true,
"default_value": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "interval_count=0;",
"se_private_data": "",
"column_key": 1,
"column_type_utf8": "varchar(320)",
"elements": [],
"collation_id": 33
}
],
"schema_ref": "my",
"hidden": false,
"se_private_id": 18446744073709551615,
"engine": "MyISAM",
"comment": "",
"se_private_data": "",
"row_format": 2,
"partition_type": 0,
"partition_expression": "",
"default_partitioning": 0,
"subpartition_type": 0,
"subpartition_expression": "",
"default_subpartitioning": 0,
"indexes": [],
"foreign_keys": [],
"partitions": [],
"collation_id": 33
}
}
这里我们创建了一个MyISAM表t1,相应的一个SDI文件被创建,文件中以JSON的格式记录了该表的详细信息。根据官方文件的描述,这个文件的存在是为了一个还未完全实现的功能。
新的Information Schema定义
一些新IS表使用View进行了重新设计,主要包括这些表:
CHARACTER_SETS
COLLATIONS
COLLATION_CHARACTER_SET_APPLICABILITY
COLUMNS
KEY_COLUMN_USAGE
SCHEMATA
STATISTICS
TABLES
TABLE_CONSTRAINTS
VIEWS
#例如SCHEMATA
mysql> show create table information_schema.schemata\G
*************************** 1. row ***************************
View: SCHEMATA
Create View: CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `information_schema`.`SCHEMATA` AS select `cat`.`name` AS `CATALOG_NAME`,`sch`.`name` AS `SCHEMA_NAME`,`cs`.`name` AS `DEFAULT_CHARACTER_SET_NAME`,`col`.`name` AS `DEFAULT_COLLATION_NAME`,NULL AS `SQL_PATH` from (((`mysql`.`schemata` `sch` join `mysql`.`catalogs` `cat` on((`cat`.`id` = `sch`.`catalog_id`))) join `mysql`.`collations` `col` on((`sch`.`default_collation_id` = `col`.`id`))) join `mysql`.`character_sets` `cs` on((`col`.`character_set_id` = `cs`.`id`))) where can_access_database(`sch`.`name`)
character_set_client: utf8
collation_connection: utf8_general_ci
1 row in set (0.01 sec)
也就是说,虽然DD系统表被隐藏不可见了,但你依然可以通过视图获得大部分信息。这种方式实际上大大加快了IS表的查询速度,转换成物理表的查询后,将无需为每个IS表的查询创建临时表(临时表的操作包含了server层创建frm, 引擎层获取数据or需要锁保护的全局数据)。另外优化器也能为IS表的查询选择更好的执行计划(例如使用系统表上的索引进行查询)。
官方对此做了测试,结果显示对IS表的查询性能大幅度提升,官方博客传送门:
MySQL 8.0: Improvements to Information_schema
MySQL 8.0: Scaling and Performance of INFORMATION_SCHEMA
新选项: information_schema_stats: CACHED | LATEST
目前表的元数据信息缓存在statistics及tables表中以加速对IS表的查询性能。你可以通过参数information_schema_stats来直接读取已经缓存到内存的数据(cached),还是从存储引擎中获取最新的数据(latest). 很显然后者要慢一点。
而从is库下,可以看到对应两种表:TABLES及TABLES_DYNAMIC, 以及STATISTICS及STATISTICS_DYNAMIC。当被设置为LATEST时,就会去从**_DYNAMIC表中去读取数据。
该选项也会影响到SHOW TABLES等语句的行为。
Data Dictionary Cache
数据词典的结构发生巨大的变化后,相应的对于内存数据词典Cache也做改动,
mysql> show variables like '%defin%';
+---------------------------------+-------+
| Variable_name | Value |
+---------------------------------+-------+
| schema_definition_cache | 256 |
| stored_program_definition_cache | 256 |
| table_definition_cache | 1400 |
| tablespace_definition_cache | 256 |
+---------------------------------+-------+
4 rows in set (0.00 sec)
tablespace_definition_cache: tablespace cache的大小,存储了tablespace的定义. 一个tablespace中可能包含多个table。
stored_program_definition_cache: 存储过程&&function的定义cache.
schema_definition_cache: 存储schema定义的cache
hardcode的字符集cache:
character set definition cache partition: Stores character set definition objects and has a hardcoded object limit of 256.
collation definition cache partition: Stores collation definition objects and has a hardcoded object limit of 256.
系统表变化
- 和权限相关的表转换成InnoDB引擎
// 包括:user, db, tables_priv, columns_priv, procs_priv, proxies_priv
// 官方博客介绍
- func表转换成InnoDB事务表
// 基于此变化,对function的操作(例如CREATE FUNCTION或者DROP FUNCTION, 或者用户定义的UDF)可能会导致一次隐式提交
- mysql库下的routine表及event表不再使用,这些信息被存储到新的DD table中,并且在mysql库下是不可见的。
- 外键系统表
// 使用两个不可见的系统表foreign_keys和foreign_key_column_usage来存储外键信息
// 由于这两个系统表不可见,你需要通过IS库下的REFERENTIAL_CONSTRAINTS和KEY_COLUMN_USAGE表来获得外键信息
// 引入的不兼容:foreign key的名字不可以超过64个字符(之前版本是允许的)
源码概览
我们回到源代码目录下,大量的新代码文件被引入,以从server层管理New DD,主要定义了一系列统一的API,代码存于sql/dd目录下,函数和类定义在namespace dd下
针对不同的元数据分别定义了不同的类及其继承关系:
namespace dd {
Weak_object
Entity_object
Dictionary_object
Tablespace
Schema
Event
Routine
Function
Procedure
Charset
Collation
Abstract_table
Table
Spatial_reference_system
Index_stat
View
Table_stat
Partition
Trigger
Index
Foreign_key
Parameter
Column
Partition_index
Partition_value
View_routine
View_table
Tablespace_file
Foreign_key_element
Index_element
Column_type_element
Parameter_type_element
Object_table
Dictionary_object_table
Object_type
Object_table_definition
}
数据词典Cache管理类:
dd::cache {
dd::cache::Dictionary_client
Object_registry
Element_map
Multi_map_base
Local_multi_map
Shared_multi_map
Cache_element
Free_list
Shared_dictionary_cache
Storage_adapter
}
mysql库存储的是系统表,但通过show tables命令,我们只能看到37个表,而从磁盘来看mysql目录下ibd文件远远超过37个,这意味着有些系统表对用户是不可见的,这些表也是用于管理核心数据词典信息,不可见的原因是避免用户不恰当的操作。(当然也不排除未来这一行为发生变化),关于这些表的访问,在目录sql/dd/impl/tables/
中进行了接口定义,这些隐藏的表包括:
$grep 'std::string s_table_name' sql/dd/impl/tables/* | awk '{ print $4}'
s_table_name("catalogs");
s_table_name("character_sets");
s_table_name("collations");
s_table_name("columns");
s_table_name("column_type_elements");
s_table_name("events");
s_table_name("foreign_key_column_usage");
s_table_name("foreign_keys");
s_table_name("index_column_usage");
s_table_name("indexes");
s_table_name("index_partitions");
s_table_name("index_stats");
s_table_name("parameters");
s_table_name("parameter_type_elements");
s_table_name("routines");
s_table_name("schemata");
s_table_name("st_spatial_reference_systems");
s_table_name("table_partitions");
s_table_name("table_partition_values");
s_table_name("tables");
s_table_name("tablespace_files");
s_table_name("tablespaces");
s_table_name("table_stats");
s_table_name("triggers");
s_table_name("version");
s_table_name("view_routine_usage");
s_table_name("view_table_usage");
我们以对一个表的常见操作为例,看看其中一些代码是如何被调用的。
(由于New DD的代码改动很大,相关的worklog有几十个,笔者通过测试+代码debug的方式第一步先熟悉代码,记录的比较凌乱)
库级操作
- 创建database
mysql> create database db1;
Query OK, 1 row affected (2.87 sec)
mysql> create database db2;
Query OK, 1 row affected (3.05 sec)
入口函数:mysql_create_db
-- 创建database目录
-- 构建binlog并写入文件
-- 调用DD API接口: dd::create_schema
* 构建对象dd::Schema
* 存储到数据词典中mysql.schemata表中,相关堆栈:
dd::create_schema
|--> dd::cache::Dictionary_client::store<dd::Schema>
|--> dd::cache::Storage_adapter::store<dd::Schema>
|--> dd::Weak_object_impl::store
|--> dd::Raw_new_record::insert
Note: schemata表对用户是不可见的
mysql> desc schemata;
ERROR 3554 (HY000): Access to system table 'mysql.schemata' is rejected.
* 创建并存储当前库的信息到SDI文件中,sdi文件命名以库名为前缀,堆栈如下
dd::create_schema
|--> dd::store_sdi
|--> dd::sdi_file::store
|--> write_sdi_file
* 成功则commit,失败则rollback
- 修改database
mysql> alter database db1 default charset gbk;
Query OK, 1 row affected (2 min 17.54 sec)
入口函数: mysql_alter_db
-- 调用DD API接口: dd::alter_schema
* 更新数据词典信息,相关堆栈:
dd::alter_schema
|--> dd::cache::Dictionary_client::update<dd::Schema>
|--> dd::cache::Dictionary_client::store<dd::Schema>
|--> dd::cache::Storage_adapter::store<dd::Schema>
|--> dd::Weak_object_impl::store
|--> dd::Raw_record::update
*更新sdi文件, 相关堆栈
dd::alter_schema
|--> dd::Sdi_updater::operator()
|--> dd::update_sdi
|--> dd::sdi_file::store
|--> write_sdi_file
*但奇怪的是,更新后很快就删除了 ?? (8.0.0版本,why ??)
看起来sdi文件的序列号没有递增,导致文件被快速删除了,实际上的目的是创建一个新的文件,写入新的数据,然后将老的SDI删掉
ref: http://bugs.mysql.com/bug.php?id=83281
-- 写Binlog
- show databases
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| db1 |
| db2 |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
6 rows in set (1.40 sec)
执行该命令时,实际上会对其进行一个SQL转换,将其转换成一个标准的查询语句,堆栈如下:
dispatch_command
|-->mysql_parse
|-->parse_sql
|-->MYSQLparse
|--> dd::info_schema::build_show_databases_query
转换后的SQL类似:
SELECT SCHEMA_NAME as `Database`,
FROM information_schema.schemata;
由于直接从系统表中读取, 这意味着在数据目录下创建一个文件夹将不会当作新的数据库目录。
- 删除database
mysql> drop database db2;
Query OK, 0 rows affected (1 min 1.86 sec)
-- 删除相关文件
-- 删除系统表mysql/schemata中记录
mysql_rm_db
|--> dd::drop_schema
|--> dd::cache::Dictionary_client::drop<dd::Schema>
|-->dd::cache::Storage_adapter::drop<dd::Schema>
|--> dd::Weak_object_impl::drop
|--> dd::Raw_record::drop
|--> handler::ha_delete_row
表级操作
- 创建表
mysql> create table t1 (a int primary key, b int, c int, key(b));
Query OK, 0 rows affected (7 min 12.29 sec)
入口函数:
mysql_create_table_no_lock
|--> create_table_impl
|--> rea_create_table
-- 先在dd中插入新的记录(dd::create_table
--> dd::create_dd_user_table
)
// 根据建表语句初始化`dd::Table` 对象,包括表的列定义,各个属性和选项,索引定义
// 存到系统表中
dd::create_dd_user_table
|--> dd::cache::Dictionary_client::store<dd::Table>
|-->dd::cache::Storage_adapter::store<dd::Table>
|-->dd::Weak_object_impl::store
// 先插入到mysql/tables系统表中
// 再插入到其他系统表中,如"mysql/columns",
|-->dd::Table_impl::store_children
|--> dd::Abstract_table_impl::store_children // mysql/columns
|--> dd::Collection<dd::Column*>::store_items
|--> Weak_object_impl::store
|-->dd::Collection<dd::Index*>::store_items // mysql/indexes
|--> dd::Weak_object_impl::store
|-->dd::Index_impl::store_children
|--> dd::Collection<dd::Index_element*>::store_items // mysql/index_column_usage
-- 然后再创建引擎文件
- Open table
-- 将实例重启后,然后再打开表,表定义第一次载入内存,需要先去访问系统表拿到表定义:
open_and_process_table
|-->open_table
|-->get_table_share_with_discover
|-->get_table_share
|-->open_table_def
// 先看schema是否存在,并从系统表`mysql/schemata`载入内存cache中
|-->dd::schema_exists
|--> dd::cache::Dictionary_client::acquire<dd::Schema>
|-->dd::cache::Dictionary_client::acquire<dd::Item_name_key, dd::Schema>
|-->dd::cache::Shared_dictionary_cache::get<dd::Item_name_key, dd::Schema>
|-->dd::cache::Shared_dictionary_cache::get_uncached<dd::Item_name_key, dd::Schema>
|-->dd::cache::Storage_adapter::get<dd::Item_name_key, dd::Schema>
|-->dd::Raw_table::find_record
// 再获取表的定义并从系统表mysql/tables载入
|-->dd::abstract_table_type
|-->dd::cache::Dictionary_client::acquire<dd::Abstract_table>
|-->dd::cache::Dictionary_client::acquire<dd::Item_name_key, dd::Abstract_table>
|-->dd::cache::Shared_dictionary_cache::get<dd::Item_name_key, dd::Abstract_table>
|-->dd::cache::Shared_dictionary_cache::get_uncached<dd::Item_name_key, dd::Abstract_table>
|-->dd::cache::Storage_adapter::get<dd::Item_name_key, dd::Abstract_table>
|--> dd::Raw_table::find_record
// 获取表上的属性信息
|-->Dictionary_object_table_impl::restore_object_from_record
|-->dd::Table_impl::restore_children
|-->dd::Abstract_table_impl::restore_children
// 从mysql/columns系统表获得列信息
|-->dd::Collection<dd::Column*>::restore_items<dd::Abstract_table_impl>
// 从mysql/indexs系统表获得索引信息
|-->dd::Collection<dd::Index*>::restore_items<dd::Table_impl>
//从mysql/index_column_usage获取索引信息
|-->dd::Collection<dd::Index_element*>::restore_items<dd::Index_impl>
// 从mysql/foreign_keys获得外键信息
|-->dd::Collection<dd::Foreign_key*>::restore_items<dd::Table_impl>
// 从mysql/table_partitions获得分区信息
|-->dd::Collection<dd::Partition*>::restore_items<dd::Table_impl>
//从"mysql/triggers获得触发器信息
|-->dd::Collection<dd::Trigger*>::restore_items<dd::Table_impl>
相关WorkLog
WL#6379: Schema definitions for new DD
WL#6380: Formulate framework for API for DD
WL#6381: Handler API changes for new dictionary
WL#6382: Define and Implement API for Table objects
WL#6383: Define and Implement API for Triggers
WL#6384: Define and Implement API for Stored Routines
WL#6385: Define and Implement API for Schema
WL#6387: Define and Implement API for Tablespaces
WL#6388: Define and Implement API for Events
WL#6389: Define and Implement API for Views
WL#6390: Use new DD API for handling non-partitioned tables
WL#6391: Protect Data Dictionary tables
WL#6392: Upgrade to Transactional Data Dictionary
WL#6394: Bootstrap code for new DD
WL#6416: InnoDB: Remove the use of *.isl files
WL#6599: New Data Dictionary and I_S integration
WL#6929: Move FOREIGN KEY constraints to the global data dictionary
WL#7053: InnoDB: Provide storage for tablespace dictionary
WL#7066: External tool to extract InnoDB tablespace dictionary information
WL#7069: Provide data dictionary information in serialized form
WL#7167: Change DDL to update rows for view columns in DD.COLUMNS and other dependent values.
WL#7284: Implement common code for different DD APIs
WL#7464: InnoDB: provide a way to do non-locking reads
WL#7488: InnoDB startup refactoring
WL#7630: Define and Implement API for Table Partition Info
WL#7771: Make sure errors are properly handled in DD API
WL#7784: Store temporary table metadata in memory
WL#7836: Use new DD API for handling partitioned tables
WL#7896: Use DD API to work with triggers
WL#7897: Use DD API to work with stored routines
WL#7898: Use DD API to work with events
WL#7907: Runtime: Use non-locking reads for DD tables under I_S view.
WL#8150: Dictionary object cache
WL#8433: Separate DD commands from regular SQL queries in the parser grammar
WL#8980: Move UDF table from MyISAM to Transactional Storage
WL#9045: Make user management DDLs atomic
官方博客:
https://mysqlserverteam.com/mysql-server-bootstrapping-and-dictionary-initialization/
https://mysqlserverteam.com/bootstrapping-the-transactional-data-dictionary/