线上一张几千万行的订单表要加一列,直接 ALTER TABLE 会不会锁表、拖垮业务?这是每个 DBA 和后端工程师都躲不开的在线 DDL难题。本文对比 MySQL 原生 Online DDL(INSTANT/INPLACE)、gh-ost 与 pt-osc 三种大表变更方案的原理、锁粒度、复制延迟与适用场景,并给出一份可直接照做的生产变更清单。
一、为什么需要在线 DDL
在 MySQL 5.6 之前,绝大多数 ALTER TABLE 都会重建整张表(COPY 算法),期间加表级锁,读写全部阻塞。一张千万级的大表,加个字段可能要几十分钟甚至几小时,业务基本处于”半瘫痪”状态。即便到了 MySQL 8.0,不是所有操作都能无锁完成——选错算法依然会长时间持锁、制造主从延迟。
所谓在线 DDL(Online DDL),指在保证表可正常读写的前提下完成结构变更。它要解决三个核心问题:① 变更期间不长时间阻塞写入;② 不把从库复制延迟拉爆;③ 失败时能安全回滚、不丢数据。下文三种方案正是在这三根绳上做取舍。
二、MySQL 原生 Online DDL:INSTANT 与 INPLACE
MySQL 8.0 把 Online DDL 拆成两个算法:INSTANT 与 INPLACE。前者只改数据字典、秒级完成;后者原地重建但不拷全表锁,仍有短暂元数据锁(MD L)。可以用 ALGORITHM 显式指定:
-- MySQL 8.0.12+:加普通列可秒级完成
ALTER TABLE orders
ADD COLUMN coupon_code VARCHAR(32) NULL,
ALGORITHM=INSTANT;
-- 不确定是否支持 INSTANT 时,用 INSTANT 试跑,不支持会直接报错提示
ALTER TABLE orders
ADD COLUMN remark TEXT,
ALGORITHM=INSTANT; -- 若返回 "Unable to execute DDL query" 说明该操作不支持
-- 降级为 INPLACE(原地重建,仍然 online,但耗时随表大小增长)
ALTER TABLE orders
ADD INDEX idx_user_status (user_id, status),
ALGORITHM=INPLACE, LOCK=NONE;
INSTANT 的支持范围随版本扩大:8.0.12 起支持加列、删列;8.0.27+ 支持加/删虚拟列、改字符集等。但它不支持加索引、改列类型、改默认值(特定场景除外)。想知道某操作走哪种算法,可以查 information_schema 或用 EXPLAIN 试跑:
-- 查看表当前是否允许 INSTANT 加列
SELECT * FROM information_schema.innodb_tables
WHERE name = 'test/orders'\G
-- 用 EXPLAIN 预览 ALTER 的执行计划(不真正执行)
EXPLAIN ALTER TABLE orders ADD COLUMN tag INT, ALGORITHM=INSTANT;
-- 结果中 Extra 显示 "Instant" 即秒级完成
| 操作类型 | INSTANT | INPLACE | COPY |
|---|---|---|---|
| 末尾加列 | ✅ 秒级 | ✅ | ✅ |
| 中间/任意位置加列(8.0.12+) | ✅ 秒级 | ✅ | ✅ |
| 加索引 | ❌ | ✅ online | ✅ |
| 改列类型(如 INT→BIGINT) | ❌ | ❌ | ✅ 锁表 |
| 改字符集 | 部分✅ | ✅ | ✅ |
可以看到,原生方案对”加列”极其友好,但对”改类型、加索引”仍要重建。真正的大表加索引、改类型,就得靠第三方工具。关于索引本身的调优思路,可延伸阅读 MySQL 性能调优实战:索引优化与慢查询分析。
三、pt-online-schema-change:触发器方案
pt-online-schema-change(pt-osc) 是 Percona Toolkit 的经典工具,思路是”影子表 + 触发器”:先建一张结构变更后的空表,通过 AFTER 触发器把原表的增量写实时同步过去,全量拷贝完成后用 RENAME 原子切换。
# 基本用法:给 orders 加列,全程不锁表
pt-online-schema-change \
--alter "ADD COLUMN coupon_code VARCHAR(32) NULL" \
--host=127.0.0.1 --port=3306 -u dba -p \
--database=test --table=orders \
--chunk-size=1000 \
--max-load="Threads_running=50" \
--critical-load="Threads_running=100" \
--recursion-method=none \
--execute
# 只打印将要执行的 SQL、不真正执行(上线前必跑)
pt-online-schema-change ... --dry-run
优点是对 MySQL 版本要求低、生态成熟;缺点是依赖触发器,会在原表上挂三个触发器,写入路径有额外开销,且 RENAME 那一刻仍需极短的元数据锁。触发器本身在某些云数据库(如 AWS RDS 部分版本)受限。
四、gh-ost:基于 binlog 的无触发器方案
gh-ost(GitHub Online Schema Transfer) 由 GitHub 开源,改用订阅 binlog 的方式回放增量,完全不碰原表触发器。它把变更在影子表上重放,再通过成熟的切换逻辑完成割接,对主库写入零侵入,对从库也更友好。
# 基础用法:连主库,通过 binlog 同步增量
gh-ost \
--host=127.0.0.1 --port=3306 --user=dba --password=xxx \
--database=test --table=orders \
--alter="ADD COLUMN coupon_code VARCHAR(32) NULL" \
--chunk-size=1000 \
--max-load=Threads_running=50 \
--throttle-control-replicas="replica1,replica2" \
--throttle-flag-file=/tmp/gh-ost.throttle \
--serve-socket-file=/tmp/gh-ost.sock \
--execute
# 紧急暂停:创建 throttle 文件即可立刻降速
touch /tmp/gh-ost.throttle
gh-ost 的最大卖点是可控:支持基于从库延迟的自动限流(--throttle-control-replicas)、用标记文件随时手动降速、甚至可以先在从库上”试跑”验证。它不依赖触发器,因此云上限制更少,是当下大表变更的首选。
五、三者对比:一张表说清怎么选
| 维度 | 原生 INSTANT | 原生 INPLACE | pt-osc | gh-ost |
|---|---|---|---|---|
| 锁粒度 | 无锁 | 短 MD L | 触发器+短 MD L | 无触发器、极短 MD L |
| 主库写入开销 | 无 | 低 | 中(触发器) | 极低 |
| 从库延迟影响 | 无 | 中 | 中 | 低(可限流) |
| 支持操作 | 仅加/删列等 | 加索引等 | 几乎全支持 | 几乎全支持 |
| 回滚难度 | 易 | 易 | 中 | 中(有 cut-over 风险) |
| 适用场景 | 8.0 加列 | 加索引 | 老版本/通用 | 大表/云上首选 |
一句话决策:能 INSTANT 就 INSTANT;加索引优先 INPLACE;改类型、大表加索引等复杂变更,新环境直接用 gh-ost,老版本或受限环境用 pt-osc。若你的变更涉及主从拓扑,记得结合 MySQL 主从复制与读写分离 一并评估从库延迟窗口。
六、生产落地清单
| 阶段 | 检查项 |
|---|---|
| 变更前 | 1. 在预发库用相同数据量 dry-run;2. 确认表无长事务、无未提交锁;3. 评估表行数选 INSTANT/INPLACE/工具;4. 准备回滚脚本 |
| 变更中 | 1. 设置 --max-load / --critical-load 限流;2. 监控主从延迟;3. 保留 throttle 文件随时降速;4. 禁止业务高峰(如大促、整点) |
| 变更后 | 1. 校验行数一致(CHECKSUM TABLE 或计数比对);2. 观察慢查询与复制延迟回归;3. 确认无残留触发器/临时表;4. 归档操作记录 |
七、常见误区与排障
误区 1:INSTANT 万能。 很多人以为 8.0 加列永远秒级,结果改个列类型直接锁表半小时。务必先 EXPLAIN ALTER 确认算法。
误区 2:忽略从库延迟。 pt-osc/gh-ost 虽然对主库友好,但全量拷贝仍会产生大量 binlog,从库追不上就会延迟飙升。用下面语句盯住延迟:
-- 在从库上查看复制延迟(MySQL 8.0 用 Replica 关键字)
SHOW REPLICA STATUS\G
-- 关注 Seconds_Behind_Source 与 SQL_Remaining_Delay
-- gh-ost 也可直接指定从库作为限流依据
SELECT
NOW() AS ts,
(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(SUBSTRING_INDEX(
SUBSTRING_INDEX(Volatile_Data, ':', -2), ',', 1))) AS lag_sec
FROM performance_schema.replication_applier_status_by_worker
LIMIT 1;
误区 3:在容器里裸跑。 工具需要稳定网络和足够的临时空间,建议把变更流程固化进 CI/CD,用隔离环境执行。可参考 Docker 容器化部署 与 GitHub Actions CI/CD 实战 把变更脚本化、可审计。遇到复制层面的延迟排查,也可对照 PostgreSQL 慢查询优化 的监控思路举一反三。
总结一下:在线 DDL 没有”唯一正确答案”,只有”最合适”。掌握原生 INSTANT/INPLACE 的边界,再在 gh-ost 与 pt-osc 之间按环境与版本取舍,配合一份扎实的变更清单,就能把大表结构变更从”提心吊胆”变成”按部就班”。




