柯煜昌 青云科技研发参谋级工程师 目前从事 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 版本,并且 innodb_file_per_table
为 on,先执行以下命令:
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)
从而开启了 ddl log
的日志,而后创立表:
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
create table
的 DDL 只有反向操作日志记录,而无清理操作日志记录。仔细的读者可能看到日志中插入某条 DDL log,随后又将其删除,会心生纳闷。但这正是 MySQL 原子 DDL 的机密所在。咱们选 DELETE SPACE
这个 DDL 日志写入函数Log_DDL::write_delete_space_log
来揭秘这个过程。
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);
}
在 create table
这个过程中调用write_delete_space_log
,is_drop
为false
,执行以上代码执行分支 (2)
和 (3)
。留神的是 insert_delete_space_log
第一个参数为空,这意味着会在创立一个后盾事务(调用trx_allocate_for_background
)插入DELETE_SPACE
记录到innodb_ddl_log
表中,而后提交该事务。留神到(3)
处delete_by_id
第一个参数为trx
, 这里的trx
即本次 DDL 的事务,(3)
所做的动作是在本次事务中删除(2)
插入的记录。
为什么是这样的逻辑呢?
以下分两种状况来探讨,如上图所示:
- 如果插入 DDL log 之后,DDL 的各个步骤都胜利执行,最初事务
trx
胜利提交,那么innodb_ddl_log
并没有该 DDL 的记录,因而在后续的post_ddl
中什么也不做(post_ddl 在前面会形容)。 - 如果插入 DDL log 之后,DDL 的某个步骤失败,则 DDL 所在的事务
trx
会回滚。此时,上图中delete [DELETE SPACE, id=20]
这个动作也会回滚。最初,innodb_ddl_log
中就会存在DELETE SPACE
这条记录,后续执行post_ddl
进行 Replay(重演),从而删除这次失败的create table
的 DDL 曾经创立的表空间。你能够发现,create table
的 DDL 创立表空间,就肯定会以这样的机制往innodb_ddl_log
中插入一条相同的动作DELETE SPACE
的日志记录,所以也被称为反向操作日志。
其它 DDL log 记录的操作如 REMOVE CACHE
、FREE
日志记录的写入也是相似的逻辑。简单的 DDL,不光是会插入反向操作日志记录,也会插入清理操作日志。比方TRUNCATE
表操作会将原有的表空间重命名为一个零时表空间,当 DDL 胜利之后,须要通过post_ddl
Replay DDL log 记录,将长期表空间删除。如果失败,又须要 post_ddl
重演 DDL log,执行反向操作,将长期表空间重命名为原来的表空间。总之,如果是反向操作日志,则应用background trx
插入并提交,而后应用trx
删除;如果是清理日志,则应用trx
插入即可。
留神:
innodb_ddl_log
表与其余 InnoDB 表一样,对该表所有操作 InnoDB 引擎都会产生 Redo 日志与 Undo 记录,所以不要将 DDL log 表中反向操作记录看作 Undo log,这两者不在同一个抽象层次上。而且反向操作在另一个事务中执行,而回滚时,Undo log 则是在原有同一个事务上执行。
须要探讨的几个问题
DDL 是否有必要日志刷盘?
咱们晓得 MySQL 有一个 innodb_flush_log_at_trx_commit
参数,当设置为 0 时,提交时并不会立即将 Redo log 刷入长久存储中。尽管能进步性能,但在掉电或者停机时会有肯定概率失落曾经提交的事务。对于 DML 操作来说,这样仅仅是失落事务,但对于 DDL 来说,失落 DDL 的事务,就会导致数据库元数据与其余数据不统一,以至数据库系统无奈失常工作。
所以,在trx_commit
会依据该事务是否为 DDL 操作,进行非凡解决:
无论 innodb_flush_log_at_trx_commit
参数如何设置,与 DDL 无关的事务,提交时必须日志刷盘!
DDL log 的写入机会
在了解了 DDL log 的机制之后,笔者问大家一个问题,对于create table
来说,是先执行write_delete_space_log
还是先创立表空间呢?
咱们先假如是先创立表空间(A 动作),再写反向操作日志(B 动作)。如果 A 执行完结后呈现掉的状况,此时 B 还未执行,此时create table
动作并没有实现,而innodb_ddl_log
不存在DELETE SPACE
这样的 DDL 反向日志记录,数据库解体复原后,数据库系统会将零碎表数据回滚,然而 A 创立的表空间却没有删除,因为存在中间状态,此时create table
就不是原子 DDL 了。
所以,在 DDL 中每个步骤中,先写入该步骤的反向操作日志记录到innodb_ddl_log
,再执行该步骤。也就是说 DDL Log 写入机会在执行步骤之前。如果create table
曾经写入了 DDL log,然而没有创立表空间就呈现掉电状况呢?这并不要紧,在 post_ddl
做 Replay 的时候,会进行解决。
Replay 的调用逻辑
在 DDL 操作实现之后,无论 DDL 的事务提交还是回滚,都会调用 post_ddl
函数,post_ddl
则会调用replay
函数进行 Replay。此外,MySQL 8.0 数据库解体复原过程中,与 MySQL 5.7 相比,也多了 ha_post_recover
的过程,它会调用log_ddl->recover
将 innodb_ddl_log
所有的日志记录进行 Replay。
在 post_ddl
调用的是replay_by_thread_id
,解体复原中ha_post_recover
调用的是replay_all
,其逻辑如下形容:
- 根据传入的
thread_id
为索引(thread_id
与trx
是能够一一对应的),以逆序形式将所有记录获取进去,而后依据记录的内容,顺次执行 Replay 动作,最初删除曾经重演的记录。 replay_all
将innodb_ddl_log
所有记录逆序形式获取进去,顺次执行 Replay 动作,最初删除曾经重演的记录。
能够看到,以上两个函数都有将记录逆序的获取的过程,为什么要逆序呢?
逆函数
1、反向操作
咱们如果将 DDL 中每个步骤看做一个函数,参数为数据库系统。假如第 i 个步骤函数为 oi,那么 n 个步骤就是 n 个函数的复合函数:
也即,复合函数的逆时所有步骤逆函数的反向复合。所以反向操作须要将 DDL log 逆序进行解决。
2、清理操作
DDL 的清理动作往往没有程序要求,逆向操作与正向操作成果往往是一样的,所以对立进行逆序解决也没有问题。
幂等性
与 Redo、Undo 相似,每个类型的日志重演均要思考其幂等性。
所谓幂等性,就是执行屡次和执行一次的成果是一样的。特地是在解体复原的时候,在重演反向操作的时候,尚未实现时产生掉电故障,从新进行解体复原。此时某项重演操作可能产生屡次。
因而,MySQL 8.0 实现这些重演操作,必须要思考幂等性。最典型是重演一些删除操作,必须先判断数据库对象是否存在。如果存在,才进行删除,否则什么都不做。
Tips:说到这里,笔者举荐一本书《具体数学:计算机科学中的一块基石》此书解说了许多计算机科学中用到的数学知识及技巧,并特地著墨于算法剖析方面。
Server 层的动作
- DDL 开始更新,无论失败与否,table share 都要进行缓存更新,tdc_remove_table;
- DDL 胜利之后,执行事务提交,否则执行事务回滚;
- 无论事务提交还是回滚,都要调用
post_ddl
,post_ddl
作用在后面曾经形容,用以 r Replay 零碎表innodb_ddl_log
记录的日志; - 解体复原时候,除了执行 Redo 日志,回滚未提交的事务之后,还须要执执行
ha_post_recover
,而 InnoDB 的ha_post_recover
就是调用post_ddl
执行 DDL 的反向操作; - binglog 解决只有一个准则,就是 DDL 事务胜利。并且提交之后,才调用
write_bin_log
写 binlog。
注意事项
- 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。
参考
- https://dev.mysql.com/doc/ref…
- https://www.slideshare.net/St…
- https://dev.mysql.com/blog-ar…