一、MySQL 原生 Online DDL 机制详解
1. 核心概念:ALGORITHM 与 LOCK 子句
MySQL 允许通过显式指定 ALGORITHM 和 LOCK 控制 DDL 行为:
sql
ALTER TABLE t
ADD INDEX idx_name (col),
ALGORITHM=INPLACE,
LOCK=NONE;
-
ALGORITHM可选值:INPLACE:在原表上操作,无需复制整表(但可能仍需重建);COPY:创建新表并逐行复制数据(等同于旧版 DDL);INSTANT(MySQL 8.0.12+):仅修改元数据,几乎零成本(仅支持部分操作,如添加列到末尾)。
-
LOCK可选值:NONE:允许并发 DML;SHARED:允许读,阻塞写;EXCLUSIVE:完全锁表。
若指定的
ALGORITHM或LOCK不被支持,语句将直接报错(而非降级),确保操作可预测。
2. InnoDB 层面的实现机制
MySQL 8.0 中,InnoDB 对 Online DDL 的支持依赖以下关键技术:
-
Row Log(重做日志缓冲) :
在
INPLACE操作期间,DML 变更被记录到一个临时的 row log 中,待 DDL 主流程完成后回放,保证数据一致性。 -
元数据锁(MDL, Metadata Lock)优化:
- 阶段1(准备):获取 排他 MDL(短暂);
- 阶段2(执行):释放排他锁,仅持 共享 MDL,允许 DML 并发;
- 阶段3(提交):再次获取排他 MDL 提交变更。
-
是否重建表?
并非所有
INPLACE操作都避免重建。例如:- 添加二级索引:不重建表;
- 修改列类型(如 VARCHAR(50) → VARCHAR(100)):需重建表 (尽管仍为
INPLACE)。
3. 支持的操作分类(MySQL 8.0)
| 操作类型 | 是否支持 LOCK=NONE |
是否重建表 | 是否 Instant |
|---|---|---|---|
| 添加二级索引 | ✅ | ❌ | ❌ |
| 删除索引 | ✅ | ❌ | ❌ |
| 添加列(末尾) | ✅ | ❌ | ✅(8.0.12+) |
| 修改列默认值 | ✅ | ❌ | ✅ |
| 修改列类型 | ❌(通常需 LOCK=SHARED) |
✅ | ❌ |
二、第三方 Online DDL 工具解析
1. pt-online-schema-change(pt-osc)
-
原理:
- 创建影子表
_t_new(结构与原表相同); - 在
_t_new上执行目标 DDL(如加索引、改列类型等); - 在原表上创建三个触发器(
INSERT/UPDATE/DELETE),用于捕获后续所有 DML 操作,并将这些变更实时应用到_t_new表中; - 开始分块(chunk-by-chunk)拷贝原表的存量数据到
_t_new:- 在此期间,任何对原表的新写入都会被触发器捕获并同步到新表;
- 因此,拷贝(存量)与同步(增量)是并发进行的;
- 当存量拷贝完成且增量同步追平后,执行原子性
RENAME。
- 创建影子表
-
优点:成熟稳定,Percona 官方维护。
-
缺点:
- 触发器带来额外开销,高并发下可能成为瓶颈;
- 主从延迟敏感(因触发器在主库执行);
- 不适用于已存在触发器的表。
使用注意点:
- 表必须有主键或唯一索引:pt-osc 依赖主键(或唯一非空索引)进行分块拷贝数据。
- 不能存在触发器(Triggers):如果原表已有同类型触发器,会导致冲突。
- 高并发写入场景下性能影响显著:触发器在每次 DML 时额外执行,增加主库 CPU 和 I/O 负载。
- 主从延迟风险:所有触发器逻辑在主库执行,从库需重放这些额外写入,可能加剧复制延迟。
- 切换阶段仍会短时锁表 :最终
RENAME操作需要获取排他元数据锁(X MDL),期间阻塞所有 DML。
2. gh-ost(GitHub Online Schema Transform)
- 原理 (无触发器设计):
- 创建
_t_gho影子表(结构同原表); - 立即在影子表上执行目标 DDL,使其具备最终 schema;
- 启动 binlog 流监听,记录当前 binlog 位点,并开始解析后续 DML 事件;
- 分批拷贝原表存量数据到
_t_gho:- 同时,根据已拷贝的数据范围,有选择地将 binlog 中的增量变更应用到
_t_gho; - 未拷贝的行变更会被忽略(后续拷贝会覆盖),已拷贝的行变更会被重放;
- 同时,根据已拷贝的数据范围,有选择地将 binlog 中的增量变更应用到
- 存量拷贝完成后,短暂锁表,执行原子性
RENAME切换; - 清理旧表。
- 创建
gh-ost 的智能判断逻辑
| 变更类型 | 行状态 | 是否需要应用到新表? | 原因 |
|---|---|---|---|
| UPDATE / DELETE / INSERT | 该行尚未被拷贝到新表 | ❌ 忽略 | 后续 SELECT 会读到最新值,直接写入新表 |
| UPDATE / DELETE | 该行已被拷贝到新表 | ✅ 必须回放 | 否则新表数据陈旧,导致不一致 |
| INSERT(新主键) | 主键 > 当前最大已拷贝 id | ❌ 忽略 | 后续拷贝会包含它 |
| INSERT(新主键) | 主键 ≤ 当前最大已拷贝 id | ✅ 必须插入 | 否则新表缺失该行 |
-
优点:
- 无触发器,对主库负载影响极小;
- 支持暂停、限速、动态调整;
- 可连接从库解析 binlog,进一步降低主库压力。
-
缺点 :依赖 binlog 格式(需
ROW),配置略复杂。
使用注意点:
-
强制依赖 binlog 格式为 ROW :gh-ost 通过解析 binlog 获取 DML 变更,必须满足:
inibinlog_format = ROW binlog_row_image = FULL -
必须指定迁移用户权限 :gh-ost 需要以下权限(以最小权限原则配置):
sqlGRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'ghost'@'%'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER ON db.* TO 'ghost'@'%'; -
连接从库可降低主库压力,但需保证一致性:若从库延迟较大,可能导致新表数据"超前"于主库,切换后短暂不一致。
-
不支持外键和某些复杂 DDL :gh-ost 不支持含外键的表 ,对
ALTER COLUMN ... SET DEFAULT等语法支持有限。 -
切换(cut-over)阶段仍存在短时锁:虽然 gh-ost 采用"原子 RENAME + 表名交换"策略,但切换瞬间仍需 X MDL。
三、方案对比:原生 vs pt-osc vs gh-ost

功能对比表
| 维度 | MySQL 原生 Online DDL | pt-online-schema-change | gh-ost |
|---|---|---|---|
| 是否锁表 | 部分操作支持 LOCK=NONE |
写操作短暂锁(切换时) | 写操作短暂锁(切换时) |
| 是否依赖触发器 | ❌ | ✅ | ❌ |
| 对主从延迟影响 | 低(仅 DDL 本身) | 中高(触发器增加主库负载) | 低(可从从库读 binlog) |
| 回滚能力 | ❌(DDL 一旦开始不可回滚) | ✅(可中止,保留原表) | ✅(可中止,保留原表) |
| 是否支持所有 DDL | ❌(有限支持) | ✅ | ✅ |
| 资源开销 | 低 | 中(触发器 + 额外连接) | 中(binlog 解析线程) |
| 工具 | 增量同步机制 | 存量与增量关系 | DDL 应用时机 |
|---|---|---|---|
| pt-osc | 触发器(Triggers) | 并发:拷贝期间触发器实时同步 | 在拷贝前应用到影子表 |
| gh-ost | Binlog 解析 | 并发:拷贝期间选择性回放 binlog | 在拷贝前应用到影子表 |
四、生产环境最佳实践与常见陷阱
最佳实践
- 优先使用原生 Online DDL :对于支持
LOCK=NONE的操作(如加索引),原生方案最轻量。 - 避免大事务期间执行 DDL :即使
LOCK=NONE,MDL 获取仍可能被长事务阻塞。 - 使用
INSTANT操作 :MySQL 8.0.12+ 中,添加列到末尾应优先尝试ALGORITHM=INSTANT。 - gh-ost 优于 pt-osc:在高并发或已有触发器的场景下,优先选择 gh-ost。
- 充分测试:在预发环境验证 DDL 对 QPS、延迟、CPU 的影响。
常见陷阱
- "假 Online" :某些操作虽声明
INPLACE,但仍需重建表(如OPTIMIZE TABLE),导致长时间 I/O 压力。 - MDL 阻塞:未提交的事务持有表级锁,导致 DDL 卡在"Waiting for metadata lock"。
- pt-osc 触发器冲突:若表已存在触发器,pt-osc 无法运行。
- gh-ost binlog 格式要求 :必须为
binlog_format=ROW,且开启binlog_row_image=FULL。
通用建议
| 项目 | 建议 |
|---|---|
| 预演测试 | 在影子库或测试环境完整执行一次 |
| 监控指标 | 主库 QPS/CPU、从库延迟、InnoDB 缓冲池命中率 |
| 回滚预案 | 明确中止命令(pt-osc:Ctrl+C;gh-ost:`echo "unpostpone" |
| DDL 语句验证 | 先在小表上验证语法与效果 |
| 避免复合操作 | 一次只做一件事(如只加索引,不要同时改列类型) |
五、面试题
Q1:MySQL Online DDL 中的 ALGORITHM=INPLACE 是否一定不锁表?
答 :否。INPLACE 仅表示不使用临时表拷贝数据,但是否锁表由 LOCK 子句决定。例如,重建表的操作即使 INPLACE,也可能需要 LOCK=SHARED。
Q2:pt-osc 和 gh-ost 的核心区别是什么?
答 :pt-osc 依赖触发器 同步 DML,而 gh-ost 通过解析 binlog 实现同步。因此 gh-ost 对主库性能影响更小,且不与现有触发器冲突。
Q3:如何判断一个 DDL 操作是否支持 LOCK=NONE?
答 :查阅 MySQL 8.0 官方文档的 Online DDL 操作表,或在测试环境执行 ALTER ... LOCK=NONE,若报错则不支持。
Q4:为什么 Online DDL 仍可能出现 "Waiting for table metadata lock"?
答 :因为 DDL 在开始和结束阶段需要获取排他元数据锁(X MDL)。若此时有未提交的事务(即使是只读事务),就会阻塞 DDL。
Q5:MySQL 8.0 的 INSTANT DDL 支持哪些操作?
答:主要包括:
- 添加列(到表末尾);
- 删除列(非末尾列需重建);
- 设置/删除列默认值;
- 修改枚举值列表(部分情况)。
注意:不能改变列顺序或数据类型。