【华为云MySQL技术专栏】MySQL Online DDL机制解析
1、背景介绍
在数据库的日常运维与使用中,数据定义语言(Data Definition Language,DDL)操作,如为表添加一个字段、增删索引、或修改列类型,是难以避免的常规需求。
然而DDL的最大痛点在于:在可能持续数小时甚至数天的DDL操作期间(对于大表而言),对原表的所有的DML(Insert/Update/Delete等)操作都会被阻塞。
为解决这一核心痛点,Online DDL功能自MySQL 5.6版本开始被引入,并在后续的5.7和8.0版本中得到了极大的增强和完善。
是否为Online DDL取决于运行过程中是否允许对表执行并发DML操作,如果允许,那么这个DDL就是Online的,否则就是Offline的。
所以说Online DDL的核心目标,在于实现表结构变更的同时,允许对该表进行并发的数据读写操作,从而最大限度地保证业务的连续性和可用性。
除MySQL官方的Online DDL方案外,也有不少优秀的第三方Online DDL解决方案。其中,Percona推出的pt-online-schema-change(pt-osc)和GitHub推出的GitHub's Online Schema-migration Tool(gh-ost)得到了广泛应用。
本文主要介绍MySQL官方的Online DDL实现机制(基于InnoDB存储引擎),以及pt-osc和gh-ost等工具的实现原理,帮助大家更安全、更高效地执行DDL操作。
2、MySQL Online DDL介绍
在MySQL Online DDL操作中,通过显式指定ALGORITHM和LOCK子句可以对表结构变更的行为进行精细控制。以加索引为例:
ALTER TABLE tbl ADD INDEX index_name(column_name), ALGORITHM=INPLACE, LOCK=NONE;
其中,ALGORITHM用于指定DDL执行的算法,LOCK则用于控制DDL期间对表并发访问的限制级别。
ALGORITHM可选取值如下:
COPY:MySQL 5.5及更早版本支持,此方式在server层创建新结构的临时表,将原表数据全部复制到临时表,完成后通过重命名替换原表。全程阻塞并发DML操作,属于Offline DDL。
INPLACE:MySQL 5.6版本引入,5.7进一步优化了稳定性和执行效率,整个DDL过程都在存储引擎内部完成,并未在server层创建临时表。对server层而言是一个“原地”操作,这就是"INPLACE"名称的来源。
INPLACE算法不阻塞并发DML的原理大致如下:在DDL执行过程中,通过row log记录并发DML,然后回放row log到新表上。根据是否需要重建表(重建主键索引),INPLACE算法可进一步分为两类:一类需重建表(需要在存储引擎层创建临时表、进行row log回放),如修改列数据类型,修改row_format,修改key_block_size,optimize table等。另一类无需重建表(无需在存储引擎层创建临时表、进行row log回放),如扩大varchar列大小(不跨越255字节)等。
INSTANT:MySQL 8.0.12版本引入,仅通过修改数据字典中的元数据即可完成结构变更,无需对数据进行任何拷贝和修改,所以通常是秒级完成DDL操作。对原表数据无影响,不阻塞并发DML。支持如下操作:添加列(在 8.0.29 之前,只能添加为表的最后一列,8.0.29 开始可以在任意位置添加列),删除列,新增或删除虚拟列,重命名列,修改或删除列的默认值,修改索引类型,重命名表,修改ENUM/SET的定义。
DEFAULT:不显式指定算法时,MySQL将按INSTANT→ INPLACE→COPY的顺序选择可用算法。
LOCK 参数用于控制DDL期间的并发访问级别,可选值为:
NONE:允许并发查询与DML操作,是实现Online DDL的理想选项。
SHARED:持有共享锁,允许执行查询操作,但阻塞DML操作。
EXCLUSIVE:持有排他锁,期间禁止查询及DML操作。
DEFAULT:由MySQL根据DDL类型自动选择所需最小锁,以平衡并发性与安全性。
LOCK参数的目的是可以手动覆盖MySQL的默认并发控制策略,在“操作速度”和“表可访问性”之间进行权衡。我们一般不指定LOCK或使用LOCK=DEFAULT,MySQL会在保证安全的前提下尽可能使用限制最少的锁来允许并发操作。
3、INPLACE DDL源码解析
下面从源码角度介绍INPLACE DDL的原理,代码基于MySQL 8.0.41,存储引擎选择innodb。
mysql_execute_command //执行sql入口函数 |->mysql_alter_table //alter table入口函数 | |->THD_STAGE_INFO(thd, stage_init); //初始化阶段 | |->mysql_inplace_alter_table //inplace ddl入口函数 | |->THD_STAGE_INFO(thd, stage_alter_inplace_prepare)// 进入准备阶段 | |->handler::ha_prepare_inplace_alter_table //准备阶段入口函数 | |->THD_STAGE_INFO(thd, stage_alter_inplace) // 进入执行阶段 | |->handler::ha_inplace_alter_table //执行阶段入口函数 | |->THD_STAGE_INFO(thd, stage_alter_inplace_commit)//进入提交阶段 | |->handler::ha_commit_inplace_alter_table //提交阶段入口函数 | |->close_all_tables_for_name //关闭所有打开的表,但保留 MDL 锁 | |->close_temporary_table //关闭所有打开的临时表 | |->trans_commit_stmt(thd) || trans_commit_implicit(thd) // 提交事务 |->thd->mdl_context.release_transactional_locks() //释放MDL锁
从以上INPLACE DDL的核心源码调用栈可以看出,INPLACE DDL大致可分为以下四个主要阶段:初始化阶段,准备阶段,执行阶段,提交阶段。
3.1 初始化阶段
|->THD_STAGE_INFO(thd, stage_init); // 进入初始化阶段
|->…… //执行各种检查和初始化操作(此处省略,只列出获取MDL锁和ALGORITHM 取值的函数调用)
|->open_tables //开表入口函数
| |->lock_table_names
| |->thd->mdl_context.acquire_locks //获取<GLOBAL,MDL_INTENTION_EXCLUSIVE>,
//<SCHEMA,MDL_INTENTION_EXCLUSIVE>,
//<TABLE,MDL_SHARED_UPGRADABLE>的锁
| |->get_and_lock_tablespace_names //获取<TABLESPACE,MDL_INTENTION_EXCLUSIVE>锁
|->check_if_supported_inplace_alter // 按 INSTANT → INPLACE → COPY 的顺序选择可用算法
在初始化阶段,会做很多检查和初始化操作,包括但不限于以下内容,其中最重要的就是获取MDL锁和调用check_if_supported_inplace_alter获取ALGORITHM的取值。
1) 初始检查和设置
-
检查是否正在修改日志表(如mysql.slow_log或mysql.general_log),如果是且日志表已启用,则报错。
-
检查是否在事务中执行ALTER TABLE,如果是则报错(因为ALTER TABLE不能在有活动事务时执行)。
-
检查ALGORITHM=INSTANT和LOCK子句的兼容性。ALGORITHM=INSTANT不能与LOCK子句一起使用(除了LOCK=DEFAULT)。
2) 表空间和分区验证
-
验证表空间名称的长度和有效性。
-
验证分区表空间名称的长度和有效性。
-
确保打开的是基表而不是视图。
3) 打开表和获取元数据锁
-
使用open_tables打开表,并获取原表的可升级共享元数据锁(MDL_SHARED_UPGRADABLE),以防止其他事务更改原表的表结构。
4) 存储引擎和表空间检查
-
设置存储引擎,如果未指定则使用原表的存储引擎。
-
检查存储引擎是否可用。
-
检查目标存储引擎是否支持表空间,并验证表空间名称的有效性。
-
检查分区表空间名称的有效性。
5) 外键约束处理
-
不允许更改有外键的表的存储引擎。
-
检查外键父表访问权限(如果添加了外键)。
-
处理外键相关的MDL锁,包括收集外键父表和子表的信息,并获取相应的MDL锁。
-
在LOCK TABLES模式下,检查所有父表是否被锁定(至少读锁),以及对于孤儿外键的子表是否被锁定(写锁)。
6) 表结构修改准备
-
准备分区信息(如果表是分区表)。
-
收集要删除的列和索引,以便后续删除统计信息。
-
创建Alter_table_ctx上下文对象,用于管理ALTER TABLE过程中的各种上下文信息。
7) 创建临时表,并在数据字典中创建对应的表定义(仅COPY算法)
8) 选择ALGORITHM
-
调用InnoDB的check_if_supported_inplace_alter方法,按INSTANT→INPLACE→COPY的顺序选择可用算法。
-
若SQL中显式指定了不支持的算法,语句将报错终止。
3.2 准备阶段
mysql_inplace_alter_table //inplace ddl入口函数(核心) |->thd->mdl_context.upgrade_shared_lock(MDL_EXCLUSIVE) //进入准备阶段前,MDL锁升级为<TABLE,MDL_EXCLUSIVE>锁 |->THD_STAGE_INFO(thd, stage_alter_inplace_prepare)// 进入准备阶段 |->handler::ha_prepare_inplace_alter_table | |->ha_innobase::prepare_inplace_alter_table | |->ha_innobase::prepare_inplace_alter_table_impl | |->prepare_inplace_alter_table_dict //更新InnoDB内部的数据字典结构,包括表对象、索引结构等,为实际的表结构变更做准备。 | |->row_mysql_lock_data_dictionary //获取数据字典排他锁 | |->row_create_table_for_mysql //创建临时表#sql-ibxxx.ibd(需要重建表时执行) | |->row_log_allocate //分配row_log对象记录并发DML产生的增量数据 | |->dd_prepare_inplace_alter_table //更新数据字典(dd)中的表空间信息,包括删除旧的表空间对象和创建新的表空间对象 | |->row_mysql_unlock_data_dictionary(trx) //释放数据字典排他锁
若选择INPLACE算法,在准备阶段主要会做如下事情:
-
更新数据字典中的表空间信息,包括删除旧的表空间对象和创建新的表空间对象。该操作之所以在 Prepare阶段做而不是Commit阶段的原因是“尽早失败”的原则。如果删除操作失败(极其罕见),整个DDL可以在Prepare阶段就回滚,成本更低。如果等到Commit阶段再做,回滚会复杂得多。
-
获取数据字典排他锁。
-
更新InnoDB内部的数据字典结构,包括表对象、索引结构等,为实际的表结构变更做准备。
-
创建与原表结构对应的临时表空间文件(#sql-ibxxx.ibd文件),用于存储重组后的数据。
-
分配row_log对象,用于在DDL执行期间并发记录其他事务产生的DML增量数据变更。
-
释放数据字典排他锁。
3.3 执行阶段
|->table->mdl_ticket->downgrade_lock //进入执行阶段前,MDL锁降级为<TABLE,MDL_SHARED_UPGRADABLE>或<TABLE,MDL_SHARED_NO_WRITE> |->THD_STAGE_INFO(thd, stage_alter_inplace) // 进入执行阶段 |->handler::ha_inplace_alter_table | |->ha_innobase::inplace_alter_table | |->ha_innobase::inplace_alter_table_impl | |->trx->isolation_level = trx_t::isolation_level_t::REPEATABLE_READ; //临时将事务隔离级别提升至 REPEATABLE READ | |->ddl.build() //执行最耗时的数据扫描和索引构建过程。读取原表的聚簇索引并排序,构建新的二级索引(需要重建表时执行) | |->row_log_table_apply //回放row log(回放的同时,row log依然在写入,提交阶段进行第二次回放) | |->trx->isolation_level = old_isolation_level; //恢复事务原有的隔离级别
若选择INPLACE算法,在执行阶段主要会做如下事情:
-
临时将事务隔离级别提升至REPEATABLE READ,防止在扫描聚簇索引构建二级索引时遇到“半写”的BLOB页或看到不一致的数据状态。
-
执行最耗时的数据扫描和索引构建过程。读取原表的聚簇索引并排序,构建新的二级索引。
-
回放row log,同步并发DML操作产生的增量数据。回放的同时,row log依然在写入,提交阶段会进行第二次回放。
-
恢复事务原有的隔离级别。
3.4 提交阶段
|->wait_while_table_is_used | |->thd->mdl_context.upgrade_shared_lock(MDL_EXCLUSIVE) //进入提交阶段前,MDL锁升级为<TABLE,MDL_EXCLUSIVE>锁 |->THD_STAGE_INFO(thd, stage_alter_inplace_commit)//进入提交阶段 |->handler::ha_commit_inplace_alter_table | |->ha_innobase::commit_inplace_alter_table | |->ha_innobase::commit_inplace_alter_table_impl | |->rollback_inplace_alter_table // 回滚alter ddl操作(若执行失败或用户手动终止) | |->ddl::lock_table(LOCK_X) //获取排他表锁 | |->row_mysql_lock_data_dictionary // 获取数据字典排他锁 | |->commit_try_rebuild //将准备阶段和执行阶段所做的修改写入数据字典(需要重建表时执行) | |-> row_log_table_apply //第二次回放:回放第一次回放row log过程中记录的row log | |->commit_try_norebuild //将准备阶段和执行阶段所做的修改写入数据字典(无需重建表时执行) | |->innobase_online_rebuild_log_free //清理row log | |->row_mysql_unlock_data_dictionary(trx) //释放数据字典排他锁 | |->dd_commit_inplace_no_change //提交数据字典的变更(无需重建表时执行) | |->dd_copy_private(Table &new_table, const Table &old_table) //将innod的私有数据(se_private_data)从旧的dd::Table拷贝到新的dd::Table | |->dd_commit_inplace_alter_table //提交数据字典的变更(需要重建表时执行)
若选择INPLACE算法,在提交阶段主要会做如下事情:
-
若执行失败或用户手动终止,则回滚DDL操作。
-
获取排他表锁。
-
获取数据字典排他锁。
-
将准备阶段和执行阶段所做的修改写入数据字典。
-
第二次回放row log。(从第一次回放row log开始,到获取排他表锁之间写入的row log)
- 回放完后清理row log。
- 释放数据字典排他锁。
- 提交数据字典的变更,将innodb的私有数据(se_private_data)从旧的dd::Table拷贝到新的dd::Table中。
3.5 MDL锁状态演变
一共有2次锁升级,1次锁降级,整个过程的MDL锁状态演变过程如下:
-
初始化阶段,打开表时获取<TABLE,MDL_SHARED_UPGRADABLE>,允许读写。
-
进入准备阶段前,将<TABLE,MDL_SHARED_UPGRADABLE>升级为<TABLE,MDL_EXCLUSIVE>,阻塞读写。
-
进入执行阶段前,若手动设置了LOCK=SHARED或DDL操作为添加FULLTEXT/SPATIAL索引,则<TABLE,MDL_EXCLUSIVE>降级为<TABLE,MDL_SHARED_NO_WRITE>(可读不可写),否则降级为<TABLE,MDL_SHARED_UPGRADABLE>(允许读写)。
-
进入提交阶段前,MDL锁再次升级为<TABLE,MDL_EXCLUSIVE>,阻塞读写。
-
执行结束,释放MDL锁。
4、第三方Online DDL解决方案
目前,第三方Online DDL解决方案也有较为广泛的使用,尤其是一些只能用官方的COPY算法的DDL操作,为了避免长时间锁表,采用第三方Online DDL解决方案也是一个不错的选择。下面介绍其中比较流行的两款工具:gh-ost和pt-osc。
4.1 gh-ost
gh-ost 是Github维护的一款开源工具,专为MySQL数据库设计,用于执行Online DDL操作,避免长时间锁表,从而减少对业务的影响。
gh-ost执行Online DDL的实现流程大致分为四个阶段:环境检查与初始化、同步数据、原子切换表名、清理资源。
1.环境检查与初始化
-
gh-ost首先会检查数据库连接权限,数据库、表、唯一键是否存在,binlog已开启且格式必须为row等。
-
创建心跳表,记录gh-ost执行日志。
-
创建与原表结构一致的影子表。
-
在影子表上执行用户下发的DDL语句。
2. 同步数据
2.1 存量数据同步
通过如下SQL将原表数据同步到影子表中。
INSERT /* gh-ost `databaseName`.`原表` */ IGNORE INTO `databaseName`.`影子表`(id,col1,col2,.... ) ( SELECT `id`, `col1`, `col2`, ... FROM `databaseName`.`原表` FORCE INDEX (PRIMARY) WHERE (rangeStartComparison and rangeEndComparison) LOCK IN SHARE MODE;)
gh-ost 通过主键将原表数据划分为多个chunk(由参数--chunk-size设置,默认1000行),分批插入到影子表中。
2.2. 增量数据同步
存量数据同步期间,gh-ost允许对原表执行并发DML操作,并发DML操作产生的增量数据的同步,gh-ost通过消费DML操作产生的binlog并在影子表上重放来实现。
2.3. 存量数据同步和增量数据同步的协同
gh-ost采用单线程同时进行存量数据和增量数据的同步,进入数据同步阶段后:
-
优先消费binlog:优先消费并发DML操作产生的binlog做增量数据同步。
-
同步存量数据:当 binlog 暂无事件时,按照 chunk 同步原表的存量数据到影子表。
-
消费同步期间的binlog:在同步存量数据期间,可能产生新的 binlog事件,因此之后立即消费这些事件。
-
循环:重复上述过程,直到所有存量数据同步完成。
3. 原子切换表名
-
锁原表:增量数据和存量数据均同步完成后,gh-ost会锁住原表以阻止新的 DML 操作,直到原子切换表名完成之后释放锁。
-
插入靶点事件:往心跳表(gh-ost创建)插入记录,产生一个 binlog 事件(靶点)。
-
增量数据同步完成:继续消费 binlog 直到看到靶点事件,由于靶点事件是锁原表之后产生的,所以靶点事件一定在所有DML操作的binlog事件之后,于是在gh-ost看到靶点时,便可确认增量数据也同步完成。
-
原子切换:通过SQL:RENAME TABLE `原表` TO `临时表`,`影子表` TO `原表`; 原子性切换表名,原子性操作,要么全部成功,要么全部失败。如果失败,原表仍存在,不影响业务,影子表会残留,需要手动清理。
4. 清理资源
删除临时表、心跳表等资源(也可选择保留),迁移完成。

gh-ost原理流程图
4.2 pt-osc
pt-osc是Percona Toolkit中的一款开源工具,专为MySQL数据库设计,用于执行Online DDL操作,避免长时间锁表,从而减少对业务的影响。
pt-osc执行Online DDL的流程大致分为四个阶段:环境检查与初始化、同步数据、原子切换表名、清理资源。
1. 环境检查与初始化
-
pt-osc首先会检查数据库连接权限,数据库、表、唯一键是否存在等。
-
创建与原表结构一致的影子表,在影子表上执行用户下发的DDL语句。
-
创建触发器:在原表上创建三个AFTER类型的触发器,分别捕获并发的INSERT、UPDATE、DELETE操作。
2.数据同步
2.1 存量数据同步
通过下面的SQL将原表数据同步到影子表中:
INSERT LOW_PRIORITY IGNORE INTO `databaseName`.`影子表` (`id`, `col1`,.....) SELECT `id`, `a` FROM `databaseName`.`原表` LOCK IN SHARE MODE /*pt-online-schema-change xxx copy table*/
pt-osc通过主键/唯一键将原表数据划分为多个chunk(由参数--chunk-size设置,默认1000行),分批插入到影子表中。
2.2 增量数据同步
从触发器创建成功开始,触发器会实时将作用于原表上的并发DML操作产生的增量数据应用到影子表,确保数据一致性。
2.3 存量数据同步和增量数据同步的协同
存量数据和增量数据分别在不同的线程中进行同步,所以存在行锁竞争,建议在业务低峰期执行,如果并发DML操作还是很频繁的话,建议调小--chunk-size。
3. 原子切换表名
存量数据和增量数据均同步完成后,通过SQL:RENAME TABLE `原表` TO `临时表`,`影子表` TO `原表`; 原子性切换表名,原子性操作,要么全部成功,要么全部失败。
4. 清理阶段
删除临时表和三个触发器等,释放资源。

pt-osc原理流程图
5、MySQL Online DDL vs gh-ost vs pt-osc
下面对MySQL Online DDL(8.0.41)、gh-ost以及pt-osc三者进行简单对比。

6、现网运维经验
最后,总结一些关于DDL的运维经验,希望对大家有帮助。
阶段一:事前准备。(最重要!)
1)全面评估。
-
操作内容:明确要执行的DDL语句是否支持INSTANT或INPLACE。
-
表大小:准确评估表行数和表空间大小。
-
业务影响:评估操作耗时(测试环境验证)以及对业务峰值时段的影响。
-
磁盘空间:确保实例有足够的空闲磁盘空间,以容纳临时日志文件、临时中间表和排序数据文件等(INSTANT算法除外)。
-
确保没有其他事务持有原表的MDL锁:长时间未提交的事务(即使只是一个简单的查询)是导致DDL操作被阻塞的最常见原因,被阻塞的DDL操作本身又会成为新的阻塞源,阻塞后续所有访问该表的事务,对业务造成严重影响。所以建议在执行DDL之前,使用show full processlist、information_schema.innodb_trx表、performance_schema.metadata_locks表、information_schema.metadata_lock_info表(仅华为云的TaurusDB和RDS for MySQL)来监控和诊断元数据锁问题,确保没有其他事务持有原表的MDL锁。
2)选择时机:必须在业务低峰期进行。
3)提前备份:确保操作失败可及时回退。
4)在从库演练:在相同规格的从库(或测试环境),用完整的生产数据副本进行全流程演练。可以准确估算执行时间,检查是否有潜在错误(如数据类型不兼容、重复键等),观察对服务器性能(IO、CPU、内存)的影响程度等。
5)合理设置工具的参数。
-
pt-osc:合理设置--max-load、--max-lag、--critical-load、--chunk-size 等参数。
-
gh-ost:合理设置--max-load、--critical-load--cut-over-lock-timeout-seconds、--max-lag-millis、--dml-batch-size等参数。
阶段二:执行过程中实时监控,监控到异常情况可适时终止DDL操作,并进行残余数据的清理。
-
数据库监控:密切关注数据库的 Threads_running、Threads_connected、CPU使用率、IOPS、网络流量和主从延迟。
-
ddl进度监控:pt-osc 和 gh-ost都会输出详细的进度信息,包括当前拷贝了多少行、预计剩余时间等,TaurusDB提供了INFORMATION_SCHEMA.INNODB_ALTER_TABLE_PROGRESS查询进度信息。
-
业务监控:关注业务系统的错误日志、响应时间和QPS是否有异常波动。
阶段三:事后验证。
-
验证表结构:操作完成后,立即检查表结构是否正确变更。
-
数据校验:对于修改数据的操作(如
MODIFY COLUMN),抽样检查一些核心数据是否正确。
-
观察业务:操作结束后,不要立即离开,持续观察业务监控,确保没有隐形问题(如因为新索引导致某些查询执行计划变化)。
-
清理:如果使用第三方工具,确认临时表和心跳表等已被成功删除,释放磁盘空间。
遵循以上建议,可以极大降低DDL操作的风险,保障数据库的稳定运行。
7、总结
本文主要从原理和源码角度分别介绍了MySQL官方的Online DDL,pt-osc和gh-ost等工具的实现机制,并对三者的机制进行对比。希望可以帮助大家更好的理解目前主流的MySQL Online DDL的解决方案,从而可以更安全、更高效的在MySQL上执行DDL操作。
- 点赞
- 收藏
- 关注作者
评论(0)