MySQL 8.0 开始支持原子数据定义语言 (DDL) 语句,即原子 DDL(atomic DDL)
。一条原子 DDL 语句将与 DDL 操作相关联的数据字典更新、存储引擎操作和二进制日志写入组合成单个原子事务。该操作要么被提交
(相应的变更被持久化到数据字典、存储引擎和二进制日志中);要么被回滚
(执行过程中出现错误,甚至是服务器宕机)。
PS:原子 DDL不是事务 DDL。一条 DDL 语句(无论是原子的还是其他的),都会隐式结束当前会话中活动的任何事务,就像在执行语句之前执行了 COMMIT 一样。这意味着 DDL 语句不能在另一个事务执行,也不能在事务控制语句: START TRANSACTION … COMMIT 中执行,或与同一事务中的其他语句组合。
MySQL 8.0 中引入 MySQL 数据字典,使原子 DDL 成为可能。在早期的 MySQL 版本中,元数据存储在元数据文件、非事务表以及存储引擎相关的字典中,这就导致 DDL 语句在执行过程中存在提交。MySQL 数据字典提供的集中式事务元数据存储解决了这一问题,从而将 DDL 语句重构成具有原子性的事务操作。
下面我们一起来学习原子 DDL 有哪些功能:
Atomic DDL
功能支持表和非表 DDL 语句。与表相关的 DDL 操作需要存储引擎支持,而非表 DDL 操作则不需要。目前只有 InnoDB 存储引擎支持 atomic DDL。
支持的表 DDL 语句包括:
支持的非表 DDL 语句包括:
create 和 drop 语句,适用于 alter 存储过程- stored programs、触发器 - triggers、视图 - views、可加载函数 - loadable functions 的语句
账户管理语句:基于 users 和 roles 的 create、alter、drop、rename(如果适用的话) 语句,以及 grant 和 revoke 语句
Atomic DDL 功能不支持以下语句:
涉及到 InnoDB 以外的存储引擎的表相关 DDL 语句。
install plugin 和 uninstall plugin 语句;
install component 和 uninstall component 语句;
create server、alter server 和 drop server 语句。
将可能存在的元数据更新、二进制日志写入和存储引擎操作被组合成单个原子操作;
在 DDL 操作期间,没有 SQL 层的中间提交。
在适用情况下:
用户可见的 DDL 操作结果具有原子性,这种方式改变了某些 DDL 操作的行为。参考:Changes in DDL Statement Behavior
如果参数中所有的表都使用了支持原子 DDL 的存储引擎,DROP TABLE 语句整体上都是一个原子操作。要么成功删除所有的表,要么回滚整个操作。
如果某个表不存在,DROP TABLE 将会返回一个错误,此时无论使用哪种存储引擎,都不会产生任何操作。以下是一个示例,由于表 t2 不存在, DROP TABLE 语句将会失败:
mysql> CREATE TABLE t1 (c1 INT);
mysql> DROP TABLE t1, t2;
ERROR 1051 (42S02): Unknown table 'test.t2'
mysql> SHOW TABLES;
+----------------+
| Tables_in_test |
+----------------+
| t1 |
+----------------+
在支持原子 DDL之前,DROP TABLE 会返回一个错误,但是仍然会删除存在的表:
mysql> CREATE TABLE t1 (c1 INT);
mysql> DROP TABLE t1, t2;
ERROR 1051 (42S02): Unknown table 'test.t2'
mysql> SHOW TABLES;
Empty set (0.00 sec)
PS:由于这种行为的变化,会在从 MySQL 5.7 主节点的上部分完成的 DROP TABLE 语句复制到 MySQL 8.0 从节点时失败。要避免这种失败场景,请在 DROP TABLE 语句中使用 IF EXISTS 语法,以防止对不存在的表发生错误。
如果所有表都使用原子 DDL 支持的存储引擎,则 DROP DATABASE 是原子的:该语句要么成功地删除所有对象,要么回滚。但是,从文件系统中删除数据库目录操作是在最后执行,并且不是原子操作的一部分。如果由于文件系统错误或服务器宕机而导致数据库目录删除失败,则不会回滚 DROP DATABASE 事务。
对于存储引擎不支持原子 DDL 操作的表,表删除操作发生在原子 DROP TABLE 或者 DROP DATABASE 事务之外。这样的表删除被单独写入二进制日志,这样在发生 DROP TABLE 或 DROP DATABASE 操作中断时,最多导致一个表的存储引擎、数据字典以及二进制日志之间存在不一致。对于删除多个表的操作,先删除不支持原子 DDL 操作的表,然后再执行原子 DDL 删除其他表。
对于存储引擎支持原子 DDL 操作的表,执行CREATE TABLE、ALTER TABLE、RENAME TABLE、TRUNCATE TABLE、CREATE TABLESPACE 和 DROP TABLESPACE 操作时,要么整体成功提交,要么在操作期间服务器故障整体回滚。在之前的MySQL版本中,这些操作的中断可能会导致存储引擎、数据字典和二进制日志之间的差异,或者留下孤儿文件。RENAME TABLE 只有当所有的表都支持原子 DDL 操作时才具有原子性。
从 MySQL 8.0.21 开始,在支持原子 DDL 的存储引擎上, CREATE TABLE … SELECT 当使用基于行的复制时,该语句将作为一个事务记录在二进制日志中。以前,它被记录为两个事务,一个用于创建表,另一个用于插入数据。两个事务之间或插入数据时的服务器故障可能会导致空表的复制。随着原子 DDL 支持的引入, CREATE TABLE … SELECT 语句现在对于基于行的复制是安全的,并且允许与 GTID-based 的复制一起使用。
在同时支持原子 DDL 和外键约束的存储引擎上,当使用基于行的复制时,不允许在 CREATE TABLE … SELECT 语句中创建外键。之后可以使用 ALTER TABLE 添加外键约束。
当 CREATE TABLE … SELECT 作为原子操作应用时,在插入数据时在表上持有元数据锁,这将防止在操作期间并发访问表。
如果要删除的视图不存在,DROP VIEW 将会失败,不会执行任何操作:
mysql> CREATE VIEW test.viewA AS SELECT * FROM t;
mysql> DROP VIEW test.viewA, test.viewB;
ERROR 1051 (42S02): Unknown table 'test.viewB'
mysql> SHOW FULL TABLES IN test WHERE TABLE_TYPE LIKE 'VIEW';
+----------------+------------+
| Tables_in_test | Table_type |
+----------------+------------+
| viewA | VIEW |
+----------------+------------+
在引入原子 DDL 之前,DROP VIEW 为不存在的命名视图返回一个错误,但是仍然会删除存在的视图:
mysql> CREATE VIEW test.viewA AS SELECT * FROM t;
mysql> DROP VIEW test.viewA, test.viewB;
ERROR 1051 (42S02): Unknown table 'test.viewB'
mysql> SHOW FULL TABLES IN test WHERE TABLE_TYPE LIKE 'VIEW';
Empty set (0.00 sec)
PS:由于这种行为的变化,在 MySQL 5.7 主节点上的部分完成的 DROP VIEW 操作,复制到 MySQL 8.0 从节点上时会失败。要避免这种失败场景,请在 DROP VIEW 语句中使用 IF EXISTS 语法,以防止对不存在的视图发生错误。
账户管理语句不再允许部分执行成功。同一个语句中的多个账户管理要么同时成功,要么全部失败。在之前的 MySQL 版本中,同时管理多个用户的账户管理语句可能对于部分用户操作成功,而另一部分用户操作失败。
mysql> CREATE USER userA;
mysql> CREATE USER userA, userB;
ERROR 1396 (HY000): Operation CREATE USER failed for 'userA'@'%'
mysql> SELECT User FROM mysql.user WHERE User LIKE 'user%';
+-------+
| User |
+-------+
| userA |
+-------+
在支持原子 DDL 之前,第二个 CREATE USER 语句虽然返回了一个错误,但是仍然创建一个新的用户:
mysql> CREATE USER userA;
mysql> CREATE USER userA, userB;
ERROR 1396 (HY000): Operation CREATE USER failed for 'userA'@'%'
mysql> SELECT User FROM mysql.user WHERE User LIKE 'user%';
+-------+
| User |
+-------+
| userA |
| userB |
+-------+
PS:由于这种行为的改变,在一个 MySQL 5.7 主节点上部分成功的账户管理语句复制到 MySQL 8.0 从节点上时将会失败。为了避免这个问题,可以使用账户管理语句的 IF EXISTS 或 IF NOT EXISTS 选项避免表相关的错误。
目前只有 InnoDB 存储引擎支持 Atomic DDL。不支持 Atomic DDL 的存储引擎不受 DDL 原子性的约束。对于不支持原子 DDL 操作的存储引擎,DDL 操作在中断或者部分执行时,仍然可能出现不数据的一致性。
为了支持 DDL 操作的重做和回滚,InnoDB 在执行过程中将 DDL 日志写入 mysql.innodb_ddl_log 表中,它是一个隐藏的数据字典表,存储在 mysql.ibd 数据字典表空间中。
要想查看 DDL 操作过程中写入 mysql.innodb_ddl_log 表中的日志,可以启用 innodb_print_ddl_logs 配置选项。参考:atomic-ddl-view-logs
PS:无论参数 innodb_flush_log_at_trx_commit 设置为何值,表 mysql.innodb_ddl_log 中数据变化的重做日志都会立即同步到磁盘中。立即刷新重做日志是为了避免出现 DDL 操作已经完成数据文件的修改,但是修改 mysql.innodb_ddl_log 表的重做日志没有持久化到磁盘中。这种情况下将会导致回滚或恢复操作的失败。
InnoDB 存储引擎执行 DDL 操作时分阶段进行处理。某些 DDL 操作,例如 ALTER TABLE,可能会在执行提交阶段之前多次执行准备阶段和执行阶段的操作。
准备:创建所需的对象,并且将 DDL 日志写入 mysql.innodb_ddl_log 表中。DDL 日志定义了如何前滚和回滚相应的 DDL 操作。
执行:执行 DDL 操作。例如,为 CREATE TABLE 执行创建操作。
提交:更数据字典并提交数据字典事务。
Post-DDL:重放并删除 mysql.innodb_ddl_log 表中的 DDL 日志。为了能够安全地执行回滚操作而不会导致不一致性,对于文件的操作,例如重命名数据文件或移动数据文件,放在这个最后的阶段执行。这个阶段还会为 DROP TABLE、TRUNCATE TABLE 以及其他重建表的 DDL 操作删除数据字典表 mysql.innodb_dynamic_metadata 中的动态元数据。
无论事务被提交还是回滚,在 Post-DDL 阶段都会重放并删除 mysql.innodb_ddl_log 表中的 DDL 日志。只有当服务器在执行 DDL 操作的过程中出现故障时,才会在 mysql.innodb_ddl_log 表中保留 DDL 日志。这种情况下,在服务器恢复之后执行 DDL 日志的重放和删除。
对于需要进行恢复的情况,服务器重启之后,可能执行 DDL 事务的提交,也可能执行事务的回滚。如果在提交阶段执行的数据字典事务已经记录在重做日志和二进制日志中,就会认为 DDL 操作已经成功,并且执行前滚操作。否则,当 InnoDB 重放数据字典重做日志的时候,将会回滚不完整的数据字典事务,并且回滚 DDL 事务。
要查看 InnoDB 存储引擎相关的原子 DDL 操作写入 mysql.innodb_ddl_log 表中的日志,可以启用 innodb_print_ddl_logs 参数,将 MySQL DDL 日志输出到 stderr。取决于操作系统和 MySQL 配置,stderr 可以是错误日志、终端或者控制台窗口。参考:error-log-destination-configuration
InnoDB 将 DDL 日志写入 mysql.innodb_ddl_log 表,用于支持 DDL 操作的重做和回滚。mysql.innodb_ddl_log 表是一个隐藏的数据字典表,存储在数据字典表空间 mysql.ibd 中。与其他的隐藏数据字典表一样,mysql.innodb_ddl_log 表不能在非调试版本的 MySQL 中直接访问。参考:data-dictionary-schema
mysql.innodb_ddl_log 表结构定义如下:
CREATE TABLE mysql.innodb_ddl_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
thread_id BIGINT UNSIGNED NOT NULL,
type INT UNSIGNED NOT NULL,
space_id INT UNSIGNED,
page_no INT UNSIGNED,
index_id BIGINT UNSIGNED,
table_id BIGINT UNSIGNED,
old_file_path VARCHAR(512) COLLATE utf8mb4_bin,
new_file_path VARCHAR(512) COLLATE utf8mb4_bin,
KEY(thread_id)
);
id:DDL 日志记录的唯一标识符。
thread_id:每个 DDL 日志记录都分配了一个thread_id,用于重放和删除属于特定 DDL 操作的 DDL 日志。涉及多个数据文件操作的 DDL 操作会生成多个 DDL 日志记录。
type:DDL 操作类型。类型包括FREE(删除索引树)、 DELETE(删除文件)、 RENAME(重命名文件)或 DROP(从 mysql.innodb_dynamic_metadata 数据字典表中删除元数据)。
space_id:表空间 ID。
page_no:包含分配信息的页面;例如,索引树根页面。
index_id:索引 ID。
table_id:表 ID。
old_file_path:旧的表空间文件路径。用于创建或删除表空间文件的 DDL 操作;也用于重命名表空间的 DDL 操作。
new_file_path:新的表空间文件路径。用于重命名表空间文件的 DDL 操作。
以下示例启用了 innodb_print_ddl_logs 参数,显示了一个 CREATE TABLE 语句输出的 DDL 日志。
mysql> SET GLOBAL innodb_print_ddl_logs=1;
mysql> CREATE TABLE t1 (c1 INT) ENGINE = InnoDB;
[Note] [000000] InnoDB: DDL log insert : [DDL record: DELETE SPACE, id=18, thread_id=7,
space_id=5, old_file_path=./test/t1.ibd]
[Note] [000000] InnoDB: DDL log delete : by id 18
[Note] [000000] InnoDB: DDL log insert : [DDL record: REMOVE CACHE, id=19, thread_id=7,
table_id=1058, new_file_path=test/t1]
[Note] [000000] InnoDB: DDL log delete : by id 19
[Note] [000000] InnoDB: DDL log insert : [DDL record: FREE, id=20, thread_id=7,
space_id=5, index_id=132, page_no=4]
[Note] [000000] InnoDB: DDL log delete : by id 20
[Note] [000000] InnoDB: DDL log post ddl : begin for thread id : 7
[Note] [000000] InnoDB: DDL log post ddl : end for thread id : 7
来自 MySQL 官方文档:Atomic-DDL ,如有翻译不对的地方欢迎指出。