背景
MySQL 8.0 原子DDL 是一个复杂的过程,涉及比较多的模块,例如:MDL 锁,表定义缓存,行格式,Row Log,DDL Log,online 属性,表空间物理文件操作等。本文主要通过与MySQL 5.7版本的对比讲述原子性相关的实现。
在 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的原子性。
例如,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
在 MySQL 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,解决元数据操作原子性
MySQL8.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 层,接口转换层,存储层。
一个典型的调用堆栈如下图所示:
原子保证(二)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事务所做的文件物理更改,有两个方面的作用:
此外,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操作:
注意:
目前腾讯云数据库 MySQL 8.0特惠活动,不限新老用户,最低5折起