背景
MySQL 8.0 DDL 是一个复杂的过程,涉及比较多的模块,例如:MDL 锁,表定义缓存,行格式,Row Log,DDL Log,online 属性,表空间物理文件操作等。本文主要通过与5.7版本的对比讲述原子性相关的实现。
相关WorkLog见:
在 8.0 之前的版本中使用了Sever层的Frm文件作为元数据保存的方式,这样做可以让多个存储引擎都使用统一的定义规范。但是也带来了一些问题,InnoDB引擎本身也做了表定义的存储,只给InnoDB引擎使用。那么Frm物理文件的操作和 InnoDB事务性表定义的更改之间如果发生Crash,就会造成Server层的元数据和InnoDB的数据不一致。
例如 Alter table 的过程中,需要产生临时表来存储新定义的表数据,如果在新旧表定义Rename过程(DDL操作的一个环节)中发生Crash,会造成表的不可访问,因为有可能FRM文件是旧的定义,但是InnoDB的同名表却是新的定义。
又例如,如果frm文件被误删除了,导致表无法被打开,如果需要删除表就需要 CDB 的 Drop Table Force功能跳过frm的检查,直接从InnoDB删除表。
MetaData Before 8.0
MySQL 8.0 的元数据结构如下所示:
详见:InnoDB INFORMATION_SCHEMA System Tables
https://dev.mysql.com/doc/refman/5.7/en/innodb-information-schema-system-tables.html
例如,ALTER TABLE 过程中涉及到的元数据信息变化如下所示:
DDL 阶段 | FRM 文件 | IBD 文件 | 信息描述 |
---|---|---|---|
Start | table_test.frm | table_test.ibd | 原表 |
DDL Prepare | table_test.frm | table_test.ibd | 原表 |
#sql-5810_3.frm | #sql-ib37-952053511.ibd | 新定义的临时表 | |
DDL Alter | 同上 | 同上 | 同上 |
DDL Commit 1 (InnoDB Commit) | 同上 | InnoDB Commit: table_test.ibd --> #sql-ib38-952053512.ibd #sql-ib37-952053511.ibd --> table_test.ibd | 将原表ibd和新定义表ibd互换名字 |
InnoDB Commit: Drop #sql-ib38-952053512.ibd | 删除原定义的表 | ||
DDL Commit 2 (Server Commit) | table_test.frm --> #sql-2add_3.frm and drop | table_test.ibd | frm文件互换名字 |
#sql-5810_3.frm --> table_test.frm | |||
Finish | table_test.frm | table_test.ibd | 新定义的表 |
如果 DDL Commit 1 阶段之后发生Crash,那么Server层和InnoDB层的表定义是不同的,表访问会失败。
MetaData After 8.0
在 8.0 中Data Dictionary 通过将系统表存储在InnoDB引擎中,构建了一套元数据存储和读取的服务框架,其中包括 DD Client 和 Storage Adaptor。
SQL层的Table Define Cache之前通过读取FRM文件来缓存定义,从而Open Table 进行访问,现在需要通过DD Client访问存储在InnoDB中的元数据。
元数据系统表有了InnoDB事务系统的支持,MySQL 8.0 将之前版本中多个事务完成的一个DDL操作变成一个 DDL Trx 事务去完成(也有其他辅助事务,但不影响DDL Trx 主导的DDL的原子性)。其实现方式就是改造元数据存储方案,将元数据和物理操作统一存储到了 InnoDB 引擎中,通过 DDL 对元数据表操作的事务的原子性,达到DDL操作的原子性。DDL Trx 事务提交则 DDL 完成,如果回滚则 DDL 执行的所有操作都可以回滚,包括:元数据表回滚和文件操作回滚。也就是原子 DDL 需要元数据操作的原子性和文件(物理)操作的原子性。
原子保证(一)InnoDB New DD,解决元数据操作原子性
8.0 中新的数据字典 Data Dictionary 是基于 InnoDB 存储引擎的事务表实现的,我们可以通过InnoDB提供的接口看到都有哪些元数据表。
通过设置 SET SESSION debug='+d,skip_dd_table_access_check'; 可以访问元数据表。
New Data Dictionary 代替了之前分散在不同地方的元数据,用于保存系统元数据,这些表会伴随着DDL的进行而进行各种操作,例如:创建一个表的时候,会向tables系统表中插入一行,会向 indexes 系统表中插入该table id以及其索引信息,多个索引就插入多个行,也会向column系统表中插入table id以及对应的列信息。值得注意的是,所有这些修改都是通过同一个DDL Trx进行的,如果事务提交则系统表的修改提交,如果DDL回滚,这些修改也会通过UNDO LOG进行回滚。Data Dictioanry 系统表解决的是之前版本Server层和InnoDB层定义不一致的问题,现在的DD tables通过InnoDB事务系统做到了原子性。
DD通过统一的接口设计提供给外层调用,其实现如下图所示:
8.0 Data Dictionary 的设计分为三层:Client 层,接口转换层,存储层。
Client层:主要负责对外提供统一访问接口以及缓存管理,SQL层的Table Define Cache就是通过DD Client 接口去获取那些之前需要从FRM文件中读的内容。同样,InnoDB层的Dict Cache也是通过DD Client读取的。
转换层:负责将Client的请求封装成对应系统表的访问方法
存储层:就是InnoDB表的存储和访问方法,和用户表一样。
原子保证(二)DDL Log 解决物理表空间文件操作原子性
DDL 操作会涉及到物理文件的操作,例如Btree的创建和释放,表空间文件ibd的创建和删除等,这样的物理操作也需要能做到可回滚,以保证DDL的原子操作。
DDL Log 被引入进来以解决物理操作的原子性,Create Table、Alter Table、Drop Table、Rename Table、Create Index 等操作都会涉及DDL Log表的修改。
DDL Log 系统表的定义如下:
mysql> show create table mysql.innodb_ddl_log \G
*************************** 1. row ***************************
Table: innodb_ddl_log
Create Table: CREATE TABLE `innodb_ddl_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`thread_id` bigint unsigned NOT NULL,
`type` int unsigned NOT NULL,
`space_id` int unsigned DEFAULT NULL,
`page_no` int unsigned DEFAULT NULL,
`index_id` bigint unsigned DEFAULT NULL,
`table_id` bigint unsigned DEFAULT NULL,
`old_file_path` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`new_file_path` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `thread_id` (`thread_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC;
DDL Log 用以记录一个DDL事务所做的文件物理更改,有两个方面的作用:
回滚的时候,为了保证DDL事务的物理文件新增操作可回滚,例如创建的ibd要删除,创建的物理索引树要释放。类似“UNDO LOG”的回滚作用。
提交之后,为了保证DDL事务的物理文件删除操作可回滚,DDL事务过程中删除操作不能立刻执行,因为一旦真正删除就不能回滚了,所以将其记录到DDL Log中。放到Commit之后再执行。
此外,Rename Log 重命名操作,Alter Table,Rename Table 会使用。
下面举例说明:
Create Table 的 DDL Log 操作(作用1):
[MY-012473] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=7, thread_id=8, space_id=4, old_file_path=./test/test.ibd]
[MY-012478] [InnoDB] DDL log delete : 7
[MY-012477] [InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=8, thread_id=8, table_id=1066, new_file_path=test/test]
[MY-012478] [InnoDB] DDL log delete : 8
[MY-012472] [InnoDB] DDL log insert : [DDL record: FREE, id=9, thread_id=8, space_id=4, index_id=156, page_no=4]
[MY-012478] [InnoDB] DDL log delete : 9
[MY-012485] [InnoDB] DDL log post ddl : begin for thread id : 8
[MY-012486] [InnoDB] DDL log post ddl : end for thread id : 8
从日志看有三种 ddl log type 的日志,日志其实描述了一个逆向操作,DDL 创建的物理文件或者索引树,这些物理操作怎么回滚,那么就写入了一个物理操作的逆向操作。
创建了表空间文件就写DELETE SPACE,创建了索引树就写 FREE TREE,内存中保留这个表的定义就写清除表定义。
值得关注的是,其中还有 DDL log delete 操作,这个其实是删除刚刚写入的 ddl log 日志。因为这些日志需要在DDL事务提交的时候全部删除,不能够保留到COMMIT之后,因为成功提交之后是不能删除这些文件和索引树的,那么这里DDL就用了DDL Trx之外的事务做 ddl log 日志的insert操作,该insert事务立刻提交,DDL trx 读取这个 ddl log record并将其标记删除,如果DDL Trx 成功Commit了,那么删除生效,ddl log 被清理。如果DDL Trx失败回滚了,那么 ddl log 日志保留下来了,按照日志的操作回滚即可。
Drop Table 的 DDL Log 操作(作用2):
[InnoDB] DDL log insert : [DDL record: DROP, id=10, thread_id=8, table_id=1066]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=11, thread_id=8, space_id=4, old_file_path=./test/test.ibd]
[InnoDB] DDL log post ddl : begin for thread id : 8
[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=11, thread_id=8, space_id=4, old_file_path=./test/test.ibd]
[InnoDB] DDL log replay : [DDL record: DROP, id=10, thread_id=8, table_id=1066]
[InnoDB] DDL log post ddl : end for thread id : 8
如上所述,Drop Table 操作 DDL Log 记录需要在 DDL Trx Commit成功后需要删除的物理操作。Drop Table需要删除独立表空间文件,就写DELETE SPACE并给出路径。和Create Table不同,这里没有delete ddl log操作,因为这些日志是需要留给Commit之后的Post DDL阶段做物理删除操作。
如果 Post DDL 阶段没有来得及做就Crash了,重启之后的会继续读取 DDL Log 表按照日志类型做相应的操作。
Alter Table 的 DDL Log 操作
// ======================Prepare=============================================
[MY-012473] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=15, thread_id=8, space_id=6, old_file_path=./test/#sql-ib1067-850981604.ibd]
[MY-012478] [InnoDB] DDL log delete : 15
[MY-012477] [InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=16, thread_id=8, table_id=1068, new_file_path=test/#sql-ib1067-850981604]
[MY-012478] [InnoDB] DDL log delete : 16
[MY-012472] [InnoDB] DDL log insert : [DDL record: FREE, id=17, thread_id=8, space_id=6, index_id=158, page_no=4]
[MY-012478] [InnoDB] DDL log delete : 17
// ======================Alter=============================================
[MY-000000] [InnoDB] TXSQL: parallel_read_threads with 1 threads, parallel_sort_threads with 1 threads, sql: alter table test add id1 int, algorithm = inplace, sample_step: 1, max_sample_cnt: 0, skip_pk_sort: 1
// ======================Commit=============================================
[MY-012475] [InnoDB] DDL log insert : [DDL record: DROP, id=18, thread_id=8, table_id=1067]
[MY-012474] [InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=19, thread_id=8, space_id=5, old_file_path=./test/#sql-ib1068-850981605.ibd, new_file_path=./test/test.ibd]
[MY-012478] [InnoDB] DDL log delete : 19
[MY-012476] [InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=20, thread_id=8, table_id=1067, old_file_path=test/#sql-ib1068-850981605, new_file_path=test/test]
[MY-012478] [InnoDB] DDL log delete : 20
[MY-012474] [InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=21, thread_id=8, space_id=6, old_file_path=./test/test.ibd, new_file_path=./test/#sql-ib1067-850981604.ibd]
[MY-012478] [InnoDB] DDL log delete : 21
[MY-012476] [InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=22, thread_id=8, table_id=1068, old_file_path=test/test, new_file_path=test/#sql-ib1067-850981604]
[MY-012478] [InnoDB] DDL log delete : 22
[MY-012475] [InnoDB] DDL log insert : [DDL record: DROP, id=23, thread_id=8, table_id=1067]
[MY-012473] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=24, thread_id=8, space_id=5, old_file_path=./test/#sql-ib1068-850981605.ibd]
[MY-012485] [InnoDB] DDL log post ddl : begin for thread id : 8
[MY-012479] [InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=24, thread_id=8, space_id=5, old_file_path=./test/#sql-ib1068-850981605.ibd]
[MY-012479] [InnoDB] DDL log replay : [DDL record: DROP, id=23, thread_id=8, table_id=1067]
[MY-012479] [InnoDB] DDL log replay : [DDL record: DROP, id=18, thread_id=8, table_id=1067]
[MY-012486] [InnoDB] DDL log post ddl : end for thread id : 8
这个是 Alter DDL 的三阶段产生的 DDL Log操作: