背景

MySQL 8.0 DDL 是一个复杂的过程,涉及比较多的模块,例如:MDL 锁,表定义缓存,行格式,Row Log,DDL Log,online 属性,表空间物理文件操作等。本文主要通过与5.7版本的对比讲述原子性相关的实现。

相关WorkLog见:

https://dev.mysql.com/worklog/task/?id=9525

https://dev.mysql.com/worklog/task/?id=9536

在 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 的元数据结构如下所示:

在 8.0 之前 MySQL 的元数据分散存储在三个不同的地方:物理文件、MyISAM引擎、InnoDB引擎。物理文件主要存储 frm,opt,trg 等定义信息,会存在与InnoDB不一致的情况,没有日志保护。系统表 user/proc/events 等信息存储在MyISAM引擎中,不支持事务。InnoDB引擎则是存储SYS_*系统表,例如SYS_TABLES,SYS_INDEXES 等。由于 物理文件和非事务引擎元数据表的存在很难做到DDL的原子性。

详见: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.frmand 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_logCreate 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事务所做的文件物理更改,有两个方面的作用:

  1. 回滚的时候,为了保证DDL事务的物理文件新增操作可回滚,例如创建的ibd要删除,创建的物理索引树要释放。类似“UNDO LOG”的回滚作用。
  2. 提交之后,为了保证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操作:

  • Prepare 阶段:创建临时名字的表用于存放新的定义的表数据,类似 Create Table。
  • Alter 阶段就是数据的转移,没有使用DDL Log。
  • Commit 阶段:InnoDB 的Alter Commit需要将InnoDB表做重命名,因此有 RENAME SPACE日志。此时的DDL Log日志仍是逆向操作的日志,代表回滚的时候要重新RENAME回来。
  • 最后Commit还需要将原表删掉,此时的原表已经是一个临时的名字了。这部分日志交给 Post DDL 阶段处理。

参考:

  1. Atomic DDL 官方介绍:
    https://dev.mysql.com/doc/refman/8.0/en/atomic-ddl.html

  2. InnoDB_New_DD:  
    Support crash-safe DDL https://dev.mysql.com/worklog/task/?id=9536

  3. Bootstrap code for new DD:
    https://dev.mysql.com/worklog/task/?id=6394

  4. MySQL 8.0 Data Dictionary:
    Background and Motivation https://dev.mysql.com/blog-archive/mysql-8-0-data-dictionary-background-and-motivation/

文章来源于腾讯云开发者社区,点击查看原文