引言
MySQL 5.6/5.7 的用户可能会发现,create一张表过程中发生crash,重启后创建一张同名新表时,会发现创建失败。这是因为过去MySQL 5.6/5.7 的DDL操作不是原子的,一张表创建失败后会遗留下ibd文件。MySQL 8.0 对DDL的实现重新进行了设计,最大的改进是DDL操作支持原子特性。由于MySQL是一个多引擎数据库,在engine层(SE)和server层(SL)都维护了自己的数据字典对象,刚接触MySQL 8.0 DDL相关源码,可能会比较困难,特别是SL中用到较多的模板方法。本文对MySQL 8.0 中DDL的进行导读性介绍,并对一些理解上比较困难的地方进行详细阐述。
为了实现DDL原子性,MySQL 8.0 使用Innodb表存储相关的数据字典信息,这些数据字典表默认不可见,查看方法参照https://dev.mysql.com/doc/refman/8.0/en/data-dictionary-schema.html
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 |
| column_statistics | 1 | System | BASE TABLE |
| column_type_elements | 1 | System | BASE TABLE |
| columns | 1 | System | BASE TABLE |
| dd_properties | 1 | System | BASE TABLE |
| events | 1 | System | BASE TABLE |
| foreign_key_column_usage | 1 | System | BASE TABLE |
| foreign_keys | 1 | System | BASE TABLE |
| index_column_usage | 1 | System | BASE TABLE |
| index_partitions | 1 | System | BASE TABLE |
| index_stats | 1 | System | BASE TABLE |
| indexes | 1 | System | BASE TABLE |
| innodb_ddl_log | 1 | System | BASE TABLE |
| innodb_dynamic_metadata | 1 | System | BASE TABLE |
| parameter_type_elements | 1 | System | BASE TABLE |
| parameters | 1 | System | BASE TABLE |
| resource_groups | 1 | System | BASE TABLE |
| routines | 1 | System | BASE TABLE |
| schemata | 1 | System | BASE TABLE |
| st_spatial_reference_systems | 1 | System | BASE TABLE |
| table_partition_values | 1 | System | BASE TABLE |
| table_partitions | 1 | System | BASE TABLE |
| table_stats | 1 | System | BASE TABLE |
| tables | 1 | System | BASE TABLE |
| tablespace_files | 1 | System | BASE TABLE |
| tablespaces | 1 | System | BASE TABLE |
| triggers | 1 | System | BASE TABLE |
| view_routine_usage | 1 | System | BASE TABLE |
| view_table_usage | 1 | System | BASE TABLE |
+------------------------------+-----------+--------+------------+
源码导读
SL的相关源码都存放在sql/dd
中,目录下dd_xxx.cc / dd_xxx.h
,为SL数据字典操作的入口。以创建表以及表数据字典对象为例,dd_table.cc
中dd::create_table
创建一个server层的,表的,数据字典对象。其类型为dd::Table
,定义在sql/dd/types/table.h
中。而dd::Table
真正的实现放在sql/dd/impl/table_impl.h / sql/dd/impl/table_impl.cc
中。
sql/dd/impl/cache
中主要是操作数据字典缓存,大都是模板类或模板方法,模板成员为SL中的各种数据字典对象(如dd::Table)。sql/dd/impl/cache/dictionary_client.cc
中实现了缓存对象的操作,包括从缓存获取、存取、丢弃等。
SE的相关源码主要存放在storage/innobase/dict/
中,主要的innodb层的数据字典内存对象是dict_index_t
, dict_table_t
。
数据字典持久化
由于篇幅有限,不能将所有的细节都阐述清楚,下面以创建一张基础表为例,简单介绍存数据字典的调用过程:
rea_create_base_table
--> dd::create_tabl // 创建数据字典对象dd::Table
--> dd::create_dd_system_table / dd::create_dd_user_table
--> dd::cache::Dictionary_client::store<dd::Table> // 存入数据字典信息
--> dd::cache::Storage_adapter::store<dd::Table>
--> dd::Table_impl::store_attributes
--> dd::Raw_record::update
--> dd::Table_impl::store_children
--> ha_create_table // 实际创建表
这里比较难以理解的是store_attributes
,store_children
,store_attributes
会存本数据字典对象的属性值; dd::Raw_record::update
调用Innodb接口,将数据字典修改持久化。同时dd::cache::Storage_adapter::store<dd::Table>
会递归调用store_children
,将与建表相关的数据字典表的也存起来:
bool Table_impl::store_children(Open_dictionary_tables_ctx *otx) {
// ...
return Abstract_table_impl::store_children(otx) ||
m_indexes.store_items(otx) || m_foreign_keys.store_items(otx) ||
m_partitions.store_items(otx) || store_triggers(otx) ||
(!skip_check_constraints && m_check_constraints.store_items(otx));
}
即调用dd::Table_impl::store_children
会同时将indexes
、foreign_keys
等数据字典表更新
原子DDL
介绍MySQL 8.0 原子DDL的资料有很多,这里以创建表为例,简单阐述创建表过程与原子DDL相关的关键流程。
为了实现原子DDL,MySQL 8.0借助mysql.innodb_ddl_log
,将DDL过程划分成以下四个步骤:
- Prpare: 写DDL logs到
mysql.innodb_ddl_log
,如对应一个per-file-table建表操作,会写入write_delete_space_log
,write_remove_cache_log
,write_free_tree_log
三条记录。 - Perform: 执行DDL操作
- Commit: 更新数据字典并且提交数据字典事务
- Post-DDL: 重放并且移除
mysql.innodb_ddl_log
对应的DDL logs
,重命名和移除大数据文件都放在这个过程中完成。
MySQL 8.0 借助Innodb的事务特性完成DDL操作。所有的DDL操作都会起一个innodb层的事务,对数据字典进行增删查改的操作,如果DDL事务执行失败,则进行回滚,这部分与正常事务是一致的。但由于DDL操作涉及文件操作,MySQL 8.0 通过DDL logs来辅助实现原子性,相关源码主要在storage/innobase/log/log0ddl.cc
。当创建一张数据表的时候,需要写一条删除索引树的记录。假设DDL操作的事务为TRX_A
,则在write_free_tree_log
中另起一个事务TRX_B
插入一条记录free tree log并马上提交,随后以TRX_A
的身份将这条记录删除。当事务commit的时候会有两种情况:
- DDL事务TRX_A正常提交,
mysql.innodb_ddl_log
中没有记录,不需要进行重放 - DDL事务TRX_A回滚,则
mysql.innodb_ddl_log
中存在一条free tree log,重放删除对应的数据文件,并移除这条记录(Log_DDL::replay
)
对于drop操作,其处理逻辑也是类似。但write_free_tree_log
中会以DDL事务TRX_A的身份写入一条free tree log,则在Post-DDL中会真正地将表删掉并移除对应的记录。
所以,mysql.innodb_ddl_log
只会在DDL过程中才会有记录。
Online DDL
Online DDL指的是在DDL期间,允许用户进行DML操作。并非所有DDL操作都支持Online DDL,官方文档 https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html 详细展示了所有DDL在执行期间是否允许进行DML操作。
这里阐述rebuild表时,Online DDL的关键流程():
- 持有
MDL_SHARED_UPGRADABLE
锁,检测表时是否存在 - 升级到
MDL_EXCLUSIVE
锁,禁止读写 - 更新数据字典对象
- 分配row_log对象记录增量
- 生成新表
- 降级为
MDL_SHARED_UPGRADABLE
,允许对原表进行读写(wait_while_table_is_used
) - 用DDL事务的上下文,扫描老表的中,对该事务可见的数据,并用merge排序,最后插入到新表,详细见
row_merge_build_indexes
。PS:由于使用到merge外排序,所以会受到innodb_sort_buffer_size
的限制。 - 在执行期间,原表的读写不阻塞,增量应用到原表中,并且会记录到row_log中
- 进入commit阶段,升级到
MDL_EXCLUSIVE
锁,禁止读写 - 在新表中apply row_log里的增量(row_log_apply)
- 更新innodb的数据字典表
- 提交DDL事务
- 重命名新表的ibd文件
值得注意的是:
这里row_log与
mysql.innodb_ddl_log
不一样,前者的源码主要在storage/innobase/row/row0log.cc
,后者的相关代码在storage/innobase/log/log0ddl.cc
。row_log是一个append形式的增量日志,里面有三种类型的记录,insert/update类型的如下所示:type: insert/update, 1 byte
old pk extra size, 1 byte
old pk, old pk size
extra size, 1~2 byte
extra data, extra size
old record data, data size
virtual column
上述第10步之后,即将锁升级为互斥锁,则可以认为已经没有事务在操作原表,否则无法升锁。所以在后续apply logs的时候,当发现delete类型的记录,则直接对新表进行purge,而不是标记为deleted mark。
- 了解上述inpalce rebuild类型的流程后,可以对官方DDL操作是否能支持online ddl有直观的理解。如drop primary key不支持online ddl,而drop primary的同时add primary却支持online ddl,两种方式都需要rebuild。但是对于前者,采用的是copy的方式而不是inplace,主要原因是新表没有主键,这种情况下innodb会默认为其申请一个隐藏的rowid作为主键,当apply logs的时候,由于log中的增量没有rowid信息,所以没法进行下去。