赞
踩
众所周知,由于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的格式记录了该表的详细信息。根据官方文件的描述,这个文件的存在是为了一个还未完全实现的功能。
一些新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等语句的行为。
数据词典的结构发生巨大的变化后,相应的对于内存数据词典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.
// 包括:user, db, tables_priv, columns_priv, procs_priv, proxies_priv
// 官方博客介绍
// 基于此变化,对function的操作(例如CREATE FUNCTION或者DROP FUNCTION, 或者用户定义的UDF)可能会导致一次隐式提交
// 使用两个不可见的系统表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的方式第一步先熟悉代码,记录的比较凌乱)
- 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
- 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
- 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;
由于直接从系统表中读取, 这意味着在数据目录下创建一个文件夹将不会当作新的数据库目录。
- 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_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>
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/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。