登录
首页 >  数据库 >  MySQL

什么是原子DDL?详解MySQL8中原子DDL的实现原理

来源:SegmentFault

时间:2023-01-18 17:26:41 295浏览 收藏

来到golang学习网的大家,相信都是编程学习爱好者,希望在这里学习数据库相关编程知识。下面本篇文章就来带大家聊聊《什么是原子DDL?详解MySQL8中原子DDL的实现原理》,介绍一下MySQL、数据库,希望对大家的知识积累有所帮助,助力实战开发!

柯煜昌 青云科技研发顾问级工程师 目前从事 RadonDB 容器化研发,华中科技大学研究生毕业,有多年的数据库内核开发经验。

文章字数 3800+,阅读时间 15 分钟

背景

MySQL 5.7 的字典信息保存在非事务表中,并且存放在不同的文件中(.FRM,.PAR,.OPT,.TRN,.TRG 等)。所有 DDL 操作都不是 Crash Safe,而且对于组合 DDL(ALTER 多个表)会出现有的成功有的失败的情况,而不是总体失败。这样主从复制就出现了问题,也导致基于复制的高可用系统不再安全。

MySQL 8.0 推出新特性 - 原子 DDL,解决了以上的问题。

什么是原子 DDL?

DDL 是指数据定义语言(Data Definition Language),负责数据结构的定义与数据对象的定义。原子 DDL 是指一个 DDL 操作是不可分割的,要么全成功要么全失败。

有哪些限制?

MySQL 8.0 只有 InnoDB 存储引擎支持原子 DDL。

支持语句:数据库、表空间、表、索引的 CREATE、ALTER 以及 DROP 语句,以及 TRUNCATE TABLE 语句。

MySQL 8.0 系统表均以 InnoDB 存储引擎存储,涉及到字典对象的均支持原子 DDL。

支持的语句:存储过程、触发器、视图以及用户定义函数(UDF)的 CREATE 和 DROP 、ALTER 操作,用户和角色的 CREATE、ALTER、DROP 语句,以及适用的 RENAME 语句,以及 GRANT 和 REVOKE 语句。

不支持的语句:

  • INSTALL PLUGIN、UNINSTALL PLUGIN

  • INSTALL COMPONENT、UNINSTALL COMPONENT

  • REATE SERVER、ALTER SERVER、DROP SERVER

实现原理是什么?

首先,8.0 将字典信息存放到事务引擎的系统表(InnoDB 存储引擎)中。这样 DDL 操作转变成一组对系统表的 DML 操作,从而失败后可以依据事务引擎自身的事务回滚保证系统表的原子性。

似乎 DDL 原子性就此就可以完成,但实际上并没有这么简单。首先字典信息不光是系统表,还有一组字典缓存,如:

  • Table Share 缓存

  • DD 缓存

  • InnoDB 中的 dict

此外,字典信息只是数据库对象的元数据,DDL 操作不光要修改字典信息,还要实实在在的操作对象,以及对象本身在内存中缓存。

  • 表空间

  • Dynamic meta

  • Btree

  • ibd 文件

  • buffer pool 中表空间的 page 页

此外,binlog 也要考虑 DDL 失败的情况。

因此,原子 DDL 在处理 DDL 失败的时候,不光是直接回滚系统表的数据,而且也要保证内存缓存,数据库对象也能回滚到一致状态。

实现细节

为了解决 DDL 失败情况中数据库对象的回滚,8.0 引入了系统表 DDL_LOG。该表在 mysql 库中。不可见,也不能人为操作。如果想了解该表的结果,先编译一个 debug 版的 MySQL:

SET SESSION debug='+d,skip_dd_table_access_check';
show create table  mysql.innodb_ddl_log;

可以看到如下表结构:

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`)
) /*!50100 TABLESPACE `mysql` */ ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC

在 8.0 中,这个表需要满足两个场景以及两个任务:

  • 场景 1:  符合 DDL 失败的场景,需要回滚部分完成的 DDL。

  • 场景 2:DDL 进行中,发生故障(掉电、软硬件故障等),重启机器需要完成部分 DDL。

两个任务:

  • 任务 1:失败后回滚,执行反向操作。

  • 任务 2:如果成功,则执行清理工作。

也许有人会问,为什么执行成功需要执行清理工作呢?

之所以要执行清理工作,因为 ibd 文件和索引一旦删除就不能恢复。为了实现回滚,DDL 删除这些对象时候,并不是真正删除,而是先将它们备份一下,以备回滚时使用。所以只有确认 DDL 已经执行成功,这些备份对象不需要了,才执行清理工作。

举个例子

为了将这个原理将清楚,我们流程相对简单的 CREATE TABLE 讲起,管中窥豹,可见一斑。假设已经有编译好了 8.0 debug 版本,并且  

mysql> set global log_error_verbosity=3;
Query OK, 0 rows affected (0.00 sec)

mysql> set global innodb_print_ddl_logs = on;
Query OK, 0 rows affected (0.00 sec)

从而开启了

mysql> create table t2 (a int);
Query OK, 0 rows affected (25 min 26.42 sec)

可以看到如下日志:

XXXXX 8 [Note] [MY-012473] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=20, thread_id=8, space_id=6, old_file_path=./test/t2.ibd]
XXXXX 8 [Note] [MY-012478] [InnoDB] DDL log delete : 20
XXXXX 8 [Note] [MY-012477] [InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=21, thread_id=8, table_id=1067, new_file_path=test/t2]
XXXXX 8 [Note] [MY-012478] [InnoDB] DDL log delete : 21
XXXXX 8 [Note] [MY-012472] [InnoDB] DDL log insert : [DDL record: FREE, id=22, thread_id=8, space_id=6, index_id=157, page_no=4]
XXXXX 8 [Note] [MY-012478] [InnoDB] DDL log delete : 22
XXXXX 8 [Note] [MY-012485] [InnoDB] DDL log post ddl : begin for thread id : 8
XXXXX 8 [Note] [MY-012486] [InnoDB] DDL log post ddl : end for thread id : 8

dberr_t Log_DDL::write_delete_space_log(trx_t *trx, const dict_table_t *table,

space_id_t space_id,

const char *file_path, bool is_drop,

bool dict_locked) {

ut_ad(trx == thd_to_trx(current_thd));

ut_ad(table == nullptr || dict_table_is_file_per_table(table));


if (skip(table, trx->mysql_thd)) {

return (DB_SUCCESS);

}


uint64_t id = next_id();

ulint thread_id = thd_get_thread_id(trx->mysql_thd);

dberr_t err;


trx->ddl_operation = true;


DBUG_INJECT_CRASH("ddl_log_crash_before_delete_space_log",

crash_before_delete_space_log_counter++);



if (is_drop) { //(1)

err = insert_delete_space_log(trx, id, thread_id, space_id, file_path,

dict_locked);

if (err != DB_SUCCESS) {

return err;

}


DBUG_INJECT_CRASH("ddl_log_crash_after_delete_space_log",

crash_after_delete_space_log_counter++);

} else { // (2)

err = insert_delete_space_log(nullptr, id, thread_id, space_id, file_path,

dict_locked);

if (err != DB_SUCCESS) {

return err;

}


DBUG_INJECT_CRASH("ddl_log_crash_after_delete_space_log",

crash_after_delete_space_log_counter++);


DBUG_EXECUTE_IF("DDL_Log_remove_inject_error_2",

srv_inject_too_many_concurrent_trxs = true;);


err = delete_by_id(trx, id, dict_locked); //(3)

ut_ad(err == DB_SUCCESS || err == DB_TOO_MANY_CONCURRENT_TRXS);


DBUG_EXECUTE_IF("DDL_Log_remove_inject_error_2",

srv_inject_too_many_concurrent_trxs = false;);


DBUG_INJECT_CRASH("ddl_log_crash_after_delete_space_delete",

crash_after_delete_space_delete_counter++);

}

return (err);

}

  • 如果插入 DDL log 之后,DDL 的各个步骤都成功执行,最后事务

  • 依据传入的

  • DDL 开始更新,无论失败与否,table share 都要进行缓存更新,tdc_remove_table;

  • DDL 成功之后,执行事务提交,否则执行事务回滚;

  • 无论事务提交还是回滚,都要调用  post_ddl ,

  • MySQL 8.0 支持原子 DDL,并不意味着 DDL 可以通过 SQL 语句命令进行回滚。实际上除了 SQLServer 外,几乎所有的数据库系统不支持 DDL 的 SQL 命令进行回滚,DDL 回滚引入的问题远远多于其带来的好处。

  • MySQL 8.0 只承诺单个 DDL 语句的原子性,并不能保证多个 DDL 组合也能保持原子性。某大厂为了实现  Truncate table flashback ,仅仅在 MySQL 的 Server 层将  truncate table  动作转换为  rename table 动作,flashback 的时候将表、索引、约束重新以 RENAME DDL 组合执行来实现 flashback,这个是及其危险的,不保证其原子性。笔者也完成过此功能,并没有如此取巧,而是老老实实的从 Server 层、InnoDB 存储引擎、binlog 各方面进行改造,完整保证其原子性。

  • MySQL 8.0 用这种方法实现原子 DDL,并不意味着其它数据库也是这种方式实现原子DDL。

  • 参考

    以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于数据库的相关知识,也可关注golang学习网公众号。

    声明:本文转载于:SegmentFault 如有侵犯,请联系study_golang@163.com删除
    相关阅读
    更多>
    最新阅读
    更多>
    课程推荐
    更多>
    评论列表