1. 前言
大家都知道元数据(Metadata)是用来描述数据的数据,没有元数据的情况下我们就没办法理解、使用数据库中存储的数据。本文通过 InnoDB 开启一个表的流程来讨论 InnoDB 的元信息管理和相关开表流程代码。
2. 元数据管理
2.1 元数据的物理存储
在 MySQL 8.0 之前,Server 层和存储引擎层会各自保留一份元数据(schema name, table definition 等),不仅在信息存储上有着重复冗余,而且可能存在两者之间存储的元数据不同步的现象。MySQL 在 8.0 中引入了 data dictionary 来进行 Server 层和不同引擎间统一的元数据管理,这些元数据都存储在 InnoDB 引擎的表中,Server 层和引擎层共享一份元数据,且支持原子性。
这些元数据对应的 InnoDB 引擎表我们一般称为系统表,其表结构是固定的直接定义在代码类结构中(因此不再需要记录额外的元数据,要不然就套娃了),对应表文件在整个 MySQL 进行初始化时就建立了,有如 tables
、columns
、indexes
、foreign_keys
等系统表。可以通过下面的 SQL 在 debug 版本查看所有系统表:
对于某张用户表来说,其元数据就是通过这些系统表对应的行内容记录构成。各元数据表的逻辑关系可以近似看成是一颗树关联结构。其顶层入口是 table 表(mysql.tables)中的对应唯一记录,再通过记录的 table id 等索引项关联到如 columns、indexes 等各元数据系统表,进而获取这个表对应的所有元数据的记录内容。
`name`
`catalog_id``name`
`table_id``name`
`table_id``name`
`...`
tables
catalogs
schemata
columns
indexes
...
2.2 元数据的内存结构
在明确了 DD 元信息在物理层面的存储格式后,我们能够很清楚的知道,当需要构建一张用户表的元数据时,我们首先需要访问所有需要的系统表(元数据表),并依次在各系统表内找到对应这张用户表的相应记录,然后用这些元数据记录构建出用户表的内存对象。
元数据表本身对应的内存对象结构是从 Object_table
类从派生出来的,有 Entity_object_table_impl
和 Object_table_impl
两大类。前者对应持久化有对应具体键对象的基本 DD 表,而后者对应着不单独存在需要通过前者关联访问的 DD 表(不能直接被 create,search,drop),例如两者分别对应派生出 Tables
、Tablespaces
、… 和 Columns
、Indexes
、… 等具体内存结构体,一一对应各元数据系统表。
值得一提,上面说的这些内存对象是元数据表本身的内存对象,也就是访问元数据表所使用的内存对象,而非某一张用户表的内存对象。类似的,对于一张用户表,也是通过对应的元数据记录构建其内存对象结构。一般从 Entity_object_impl
类从派生出来的,像常用的 Table_impl
和 View_impl
分别表示用户表和视图对应元数据的内存对象。
当缓存穿透时,这些 objects 的底层操作逻辑被封装在 Storage_adapter
中,通过提供的 get() / drop() / store() 等接口,遍历查询或修改所有所需元数据表(btree 结构)中对应的 record,构建或持久性相应的 object 内存对象由/到引擎层。
2.3 元数据的 cache 缓存
从上述操作中可以看出,直接从元数据表构建 DD object 内存对象是开销十分巨大的,过程中需要物理访问并索引多个 Btree 索引。因此 MySQL 建立了多层的 DD Cache 来就可能减小元数据的访问开销,一般称为元数据的 3 层缓存架构:
- 每个 client 的独享缓存,即线程 THD 独占的
Dictionary_client
结构,其中有 committed、uncommitted、dropped 三类 objects map,最初 acquire 的 object 会被加入到committed map 中,client 调用 store 或 update 接口时将 object 放到 uncommitted map 中,然后在事务提交后将相应 objects 从 uncommitted map 移到 committed map 中,而调用 drop 接口会将 objects 加入 dropped map。当访问 Dictionary_client 穿透时,从 Shared_dictionary_cache 获取; - Server 全局唯一的共享缓存,使用单例
Shared_dictionary_cache
来实现,其实质上也是 objects map 集合。在此缓存层相同 key 对应的 DD objects 对象唯一,这里的 key 其实就是对应元数据表的索引 key。当 Shared_dictionary_cache 穿透时,通过Storage_adapter
从 InnoDB handler 读取元数据表记录。 - 存储引擎层(元数据表数据直接 BP 缓存),系统表的访问模式和普通用户表基本一致,注意采用的是 READ_COMMITTED 隔离级别。
3. 元数据的使用
3.1 Server 层元数据的使用
实现了 DD 元数据的管理,MySQL 就能够通过使用元数据构建访问用户表的环境,这也就是我们常说的“开表”逻辑。前面已经介绍了元数据表本身的内存对象,通过访问元数据表相应记录,类似的可以构建用户表(或其他内容,如视图等)的内存对象。
我们需要知道 Server 层的两个关键结构体 TABLE_SHARE
和 TABLE
:
- 一张表被初次访问时,MySQL 会其建立一个 TABLE_SHARE 对象,与其与引擎层中的对应表
dict_table_t
相对应关联。TABLE_SHARE 是静态的,不能修改的,且一张表只存在一份,其中记录表定义相关的一些 DD 信息,如包含的字段等。TABLE_SHARE 只有在表结构被修改后才会删除,或者缓存使用满了会淘汰。简单的说,TABLE_SHARE 就是某张表定义的实体化对象。 - 对每一个会话查询中涉及的表,MySQL 会通过 TABLE_SHARE 为每个表建一个 TABLE实体对象,这一过程叫表结构实例化。如果是 InnoDB 表还会创建 InnoDB 的 handler,server 层会话通过 TABLE 对象经引擎层操作表文件实体。可以将 TABLE 对象看做表在 server 层的映射,将 handler 看做其为操作底层数据文件而在引擎层创建的句柄。
“开表”逻辑就是通过访问元数据来获取 TABLE_SHARE 和构建 TABLE 实体对象的过程。我们具体看下 open_table
的代码逻辑。
|
SELECT |
考虑所有 cache 都穿透的情况,则此时在 get_table_share
中,通过 DD 接口从元数据表读取相应(表)对象的元数据记录,再以之填充新生成的 TABLE_SHARE。
3.2 Server 层表对象缓存
从前面 open_table
代码可见,获取 TABLE_SHARE 和构建 TABLE 实体对象过程中也涉及多层 cache 缓存机制。首先是 Table_cache_manager
缓存了 TABLE 对象,维护了所有正在使用或曾经打开过的 TABLE 对象,其大小由 table_cache_size 维护,内部按 THD 分片为 table_cache_instances 个 Table_cache
。每个 Table_cache
内部由 object name(例如某张用户表) 映射到 Table_cache_element
,可见 Table_cache_element
唯一对应一个 object,因此也唯一对应一个 TABLE_SHARE,其内部链接了这个缓存分片内中的所有由此 TABLE_SHARE 生成的 TABLE 实例。
如果 Table_cache_manager
缓存穿透,则会去 Table_definition_cache
缓存寻找是否有存在 TABLE_SHARE 对象,其大小设置为 min(400 + table_cache_size / 2, 2000)
。 如果 Table_definition_cache
进一步穿透,则会去 InnoDB 层读取元数据构建 TABLE_SHARE。
另外,在 InnoDB 也为每一个 InnoDB 表加载一个数据字典对象,这些对象的集合就是 InnoDB 中的 data dictionary。InnoDB 的 dictionary system 以 全局 dict_sys_t
管理,而单个表对象对应 dict_table_t
,类似的,索引对象对应 dict_index_t
,列对象对应 dict_col_t
等。InnoDB 同样通过读取元数据表记录来构建 dict_table_t
对象,并且 dict_sys_t
中也有两个 dict_table_t
缓存,分别以 table name 和 table id 进行映射关联,其最大容量限制和 Table_definition_cache
一致。
1. 前言
大家都知道元数据(Metadata)是用来描述数据的数据,没有元数据的情况下我们就没办法理解、使用数据库中存储的数据。本文通过 InnoDB 开启一个表的流程来讨论 InnoDB 的元信息管理和相关开表流程代码。
2. 元数据管理
2.1 元数据的物理存储
在 MySQL 8.0 之前,Server 层和存储引擎层会各自保留一份元数据(schema name, table definition 等),不仅在信息存储上有着重复冗余,而且可能存在两者之间存储的元数据不同步的现象。MySQL 在 8.0 中引入了 data dictionary 来进行 Server 层和不同引擎间统一的元数据管理,这些元数据都存储在 InnoDB 引擎的表中,Server 层和引擎层共享一份元数据,且支持原子性。
这些元数据对应的 InnoDB 引擎表我们一般称为系统表,其表结构是固定的直接定义在代码类结构中(因此不再需要记录额外的元数据,要不然就套娃了),对应表文件在整个 MySQL 进行初始化时就建立了,有如 tables
、columns
、indexes
、foreign_keys
等系统表。可以通过下面的 SQL 在 debug 版本查看所有系统表:
对于某张用户表来说,其元数据就是通过这些系统表对应的行内容记录构成。各元数据表的逻辑关系可以近似看成是一颗树关联结构。其顶层入口是 table 表(mysql.tables)中的对应唯一记录,再通过记录的 table id 等索引项关联到如 columns、indexes 等各元数据系统表,进而获取这个表对应的所有元数据的记录内容。
`name``catalog_id``name``table_id``name``table_id``name``...`tablescatalogsschematacolumnsindexes...
2.2 元数据的内存结构
在明确了 DD 元信息在物理层面的存储格式后,我们能够很清楚的知道,当需要构建一张用户表的元数据时,我们首先需要访问所有需要的系统表(元数据表),并依次在各系统表内找到对应这张用户表的相应记录,然后用这些元数据记录构建出用户表的内存对象。
元数据表本身对应的内存对象结构是从 Object_table
类从派生出来的,有 Entity_object_table_impl
和 Object_table_impl
两大类。前者对应持久化有对应具体键对象的基本 DD 表,而后者对应着不单独存在需要通过前者关联访问的 DD 表(不能直接被 create,search,drop),例如两者分别对应派生出 Tables
、Tablespaces
、… 和 Columns
、Indexes
、… 等具体内存结构体,一一对应各元数据系统表。
值得一提,上面说的这些内存对象是元数据表本身的内存对象,也就是访问元数据表所使用的内存对象,而非某一张用户表的内存对象。类似的,对于一张用户表,也是通过对应的元数据记录构建其内存对象结构。一般从 Entity_object_impl
类从派生出来的,像常用的 Table_impl
和 View_impl
分别表示用户表和视图对应元数据的内存对象。
当缓存穿透时,这些 objects 的底层操作逻辑被封装在 Storage_adapter
中,通过提供的 get() / drop() / store() 等接口,遍历查询或修改所有所需元数据表(btree 结构)中对应的 record,构建或持久性相应的 object 内存对象由/到引擎层。
2.3 元数据的 cache 缓存
从上述操作中可以看出,直接从元数据表构建 DD object 内存对象是开销十分巨大的,过程中需要物理访问并索引多个 Btree 索引。因此 MySQL 建立了多层的 DD Cache 来就可能减小元数据的访问开销,一般称为元数据的 3 层缓存架构:
- 每个 client 的独享缓存,即线程 THD 独占的
Dictionary_client
结构,其中有 committed、uncommitted、dropped 三类 objects map,最初 acquire 的 object 会被加入到committed map 中,client 调用 store 或 update 接口时将 object 放到 uncommitted map 中,然后在事务提交后将相应 objects 从 uncommitted map 移到 committed map 中,而调用 drop 接口会将 objects 加入 dropped map。当访问 Dictionary_client 穿透时,从 Shared_dictionary_cache 获取; - Server 全局唯一的共享缓存,使用单例
Shared_dictionary_cache
来实现,其实质上也是 objects map 集合。在此缓存层相同 key 对应的 DD objects 对象唯一,这里的 key 其实就是对应元数据表的索引 key。当 Shared_dictionary_cache 穿透时,通过Storage_adapter
从 InnoDB handler 读取元数据表记录。 - 存储引擎层(元数据表数据直接 BP 缓存),系统表的访问模式和普通用户表基本一致,注意采用的是 READ_COMMITTED 隔离级别。
3. 元数据的使用
3.1 Server 层元数据的使用
实现了 DD 元数据的管理,MySQL 就能够通过使用元数据构建访问用户表的环境,这也就是我们常说的“开表”逻辑。前面已经介绍了元数据表本身的内存对象,通过访问元数据表相应记录,类似的可以构建用户表(或其他内容,如视图等)的内存对象。
我们需要知道 Server 层的两个关键结构体 TABLE_SHARE
和 TABLE
:
- 一张表被初次访问时,MySQL 会其建立一个 TABLE_SHARE 对象,与其与引擎层中的对应表
dict_table_t
相对应关联。TABLE_SHARE 是静态的,不能修改的,且一张表只存在一份,其中记录表定义相关的一些 DD 信息,如包含的字段等。TABLE_SHARE 只有在表结构被修改后才会删除,或者缓存使用满了会淘汰。简单的说,TABLE_SHARE 就是某张表定义的实体化对象。 - 对每一个会话查询中涉及的表,MySQL 会通过 TABLE_SHARE 为每个表建一个 TABLE实体对象,这一过程叫表结构实例化。如果是 InnoDB 表还会创建 InnoDB 的 handler,server 层会话通过 TABLE 对象经引擎层操作表文件实体。可以将 TABLE 对象看做表在 server 层的映射,将 handler 看做其为操作底层数据文件而在引擎层创建的句柄。
“开表”逻辑就是通过访问元数据来获取 TABLE_SHARE 和构建 TABLE 实体对象的过程。我们具体看下 open_table
的代码逻辑。
|
SELECT |
考虑所有 cache 都穿透的情况,则此时在 get_table_share
中,通过 DD 接口从元数据表读取相应(表)对象的元数据记录,再以之填充新生成的 TABLE_SHARE。
3.2 Server 层表对象缓存
从前面 open_table
代码可见,获取 TABLE_SHARE 和构建 TABLE 实体对象过程中也涉及多层 cache 缓存机制。首先是 Table_cache_manager
缓存了 TABLE 对象,维护了所有正在使用或曾经打开过的 TABLE 对象,其大小由 table_cache_size 维护,内部按 THD 分片为 table_cache_instances 个 Table_cache
。每个 Table_cache
内部由 object name(例如某张用户表) 映射到 Table_cache_element
,可见 Table_cache_element
唯一对应一个 object,因此也唯一对应一个 TABLE_SHARE,其内部链接了这个缓存分片内中的所有由此 TABLE_SHARE 生成的 TABLE 实例。
如果 Table_cache_manager
缓存穿透,则会去 Table_definition_cache
缓存寻找是否有存在 TABLE_SHARE 对象,其大小设置为 min(400 + table_cache_size / 2, 2000)
。 如果 Table_definition_cache
进一步穿透,则会去 InnoDB 层读取元数据构建 TABLE_SHARE。
另外,在 InnoDB 也为每一个 InnoDB 表加载一个数据字典对象,这些对象的集合就是 InnoDB 中的 data dictionary。InnoDB 的 dictionary system 以 全局 dict_sys_t
管理,而单个表对象对应 dict_table_t
,类似的,索引对象对应 dict_index_t
,列对象对应 dict_col_t
等。InnoDB 同样通过读取元数据表记录来构建 dict_table_t
对象,并且 dict_sys_t
中也有两个 dict_table_t
缓存,分别以 table name 和 table id 进行映射关联,其最大容量限制和 Table_definition_cache
一致。
|
// Step 1. 除特殊 DD 表等场景,LOCK TABLES mode(LTM)下校验是否表对象都是 pre-opened 的; // Step 2. 非 LTM 模式(通常的模式),根据 mdl 请求模式获取 mdl 锁; if (open_table_get_mdl_lock(thd, ot_ctx, table_list, flags, &mdl_ticket) || // Step 3. 检查目标表的存在性; /* Deadlock or timeout occurred while upgrading the lock. */ // Step 4. Table 存在,尝试开表,首先从 table_cache_manager 这个缓存找 if (!table_list->is_view()) if (table) { /* Case 1. 找到未使用的 TABLE object */ table->file->rebind_psi(); // Step 5. 获取 TABLE_SHARE: if (table_list->is_view() || share->is_view) { share_found: MDL_deadlock_handler mdl_deadlock_handler(ot_ctx); thd->push_internal_handler(&mdl_deadlock_handler); goto retry_share; if (thd->open_tables && // Step 6. 由 TABLE_SHARE 构建 TABLE 对象 error = open_table_from_share( if (error) { table_found: // 当前有了 TABLE 对象 reset: // 成功,初始化返回 table_list->set_updatable(); // skipping partitions bitmap setting in MYSQL_OPEN_NO_NEW_TABLE_IN_SE table->init(thd, table_list); return false; err_lock: // 失败 return true; |
|
// Step 1. 除特殊 DD 表等场景,LOCK TABLES mode(LTM)下校验是否表对象都是 pre-opened 的; // Step 2. 非 LTM 模式(通常的模式),根据 mdl 请求模式获取 mdl 锁; if (open_table_get_mdl_lock(thd, ot_ctx, table_list, flags, &mdl_ticket) || // Step 3. 检查目标表的存在性; /* Deadlock or timeout occurred while upgrading the lock. */ // Step 4. Table 存在,尝试开表,首先从 table_cache_manager 这个缓存找 if (!table_list->is_view()) if (table) { /* Case 1. 找到未使用的 TABLE object */ table->file->rebind_psi(); // Step 5. 获取 TABLE_SHARE: if (table_list->is_view() || share->is_view) { share_found: MDL_deadlock_handler mdl_deadlock_handler(ot_ctx); thd->push_internal_handler(&mdl_deadlock_handler); goto retry_share; if (thd->open_tables && // Step 6. 由 TABLE_SHARE 构建 TABLE 对象 error = open_table_from_share( if (error) { table_found: // 当前有了 TABLE 对象 reset: // 成功,初始化返回 table_list->set_updatable(); // skipping partitions bitmap setting in MYSQL_OPEN_NO_NEW_TABLE_IN_SE table->init(thd, table_list); return false; err_lock: // 失败 return true; |