[小技巧60]深入解析 MySQL Online DDL:MySQL Online DDL、pt-osc 与 gh-ost 机制与最佳实践

一、MySQL 原生 Online DDL 机制详解

1. 核心概念:ALGORITHMLOCK 子句

MySQL 允许通过显式指定 ALGORITHMLOCK 控制 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:完全锁表。

若指定的 ALGORITHMLOCK 不被支持,语句将直接报错(而非降级),确保操作可预测。

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)。

参考:MySQL 8.0 官方文档 - Online DDL Operations

3. 支持的操作分类(MySQL 8.0)

操作类型 是否支持 LOCK=NONE 是否重建表 是否 Instant
添加二级索引
删除索引
添加列(末尾) ✅(8.0.12+)
修改列默认值
修改列类型 ❌(通常需 LOCK=SHARED

二、第三方 Online DDL 工具解析

1. pt-online-schema-change(pt-osc)

  • 原理:

    1. 创建影子表 _t_new(结构与原表相同);
    2. _t_new 上执行目标 DDL(如加索引、改列类型等);
    3. 在原表上创建三个触发器(INSERT / UPDATE / DELETE),用于捕获后续所有 DML 操作,并将这些变更实时应用到 _t_new 表中;
    4. 开始分块(chunk-by-chunk)拷贝原表的存量数据到 _t_new
      • 在此期间,任何对原表的新写入都会被触发器捕获并同步到新表;
      • 因此,拷贝(存量)与同步(增量)是并发进行的;
    5. 当存量拷贝完成且增量同步追平后,执行原子性 RENAME
  • 优点:成熟稳定,Percona 官方维护。

  • 缺点

    • 触发器带来额外开销,高并发下可能成为瓶颈;
    • 主从延迟敏感(因触发器在主库执行);
    • 不适用于已存在触发器的表。

使用注意点:

  • 表必须有主键或唯一索引:pt-osc 依赖主键(或唯一非空索引)进行分块拷贝数据。
  • 不能存在触发器(Triggers):如果原表已有同类型触发器,会导致冲突。
  • 高并发写入场景下性能影响显著:触发器在每次 DML 时额外执行,增加主库 CPU 和 I/O 负载。
  • 主从延迟风险:所有触发器逻辑在主库执行,从库需重放这些额外写入,可能加剧复制延迟。
  • 切换阶段仍会短时锁表 :最终 RENAME 操作需要获取排他元数据锁(X MDL),期间阻塞所有 DML。

2. gh-ost(GitHub Online Schema Transform)

  • 原理 (无触发器设计):
    1. 创建 _t_gho 影子表(结构同原表);
    2. 立即在影子表上执行目标 DDL,使其具备最终 schema;
    3. 启动 binlog 流监听,记录当前 binlog 位点,并开始解析后续 DML 事件;
    4. 分批拷贝原表存量数据到 _t_gho
      • 同时,根据已拷贝的数据范围,有选择地将 binlog 中的增量变更应用到 _t_gho
      • 未拷贝的行变更会被忽略(后续拷贝会覆盖),已拷贝的行变更会被重放;
    5. 存量拷贝完成后,短暂锁表,执行原子性 RENAME 切换;
    6. 清理旧表。

gh-ost 的智能判断逻辑

变更类型 行状态 是否需要应用到新表? 原因
UPDATE / DELETE / INSERT 该行尚未被拷贝到新表 ❌ 忽略 后续 SELECT 会读到最新值,直接写入新表
UPDATE / DELETE 该行已被拷贝到新表 ✅ 必须回放 否则新表数据陈旧,导致不一致
INSERT(新主键) 主键 > 当前最大已拷贝 id ❌ 忽略 后续拷贝会包含它
INSERT(新主键) 主键 ≤ 当前最大已拷贝 id ✅ 必须插入 否则新表缺失该行
  • 优点

    • 无触发器,对主库负载影响极小;
    • 支持暂停、限速、动态调整;
    • 可连接从库解析 binlog,进一步降低主库压力。
  • 缺点 :依赖 binlog 格式(需 ROW),配置略复杂。

使用注意点:

  • 强制依赖 binlog 格式为 ROW :gh-ost 通过解析 binlog 获取 DML 变更,必须满足:

    ini 复制代码
    binlog_format = ROW
    binlog_row_image = FULL
  • 必须指定迁移用户权限 :gh-ost 需要以下权限(以最小权限原则配置):

    sql 复制代码
    GRANT 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 在拷贝前应用到影子表

四、生产环境最佳实践与常见陷阱

最佳实践

  1. 优先使用原生 Online DDL :对于支持 LOCK=NONE 的操作(如加索引),原生方案最轻量。
  2. 避免大事务期间执行 DDL :即使 LOCK=NONE,MDL 获取仍可能被长事务阻塞。
  3. 使用 INSTANT 操作 :MySQL 8.0.12+ 中,添加列到末尾应优先尝试 ALGORITHM=INSTANT
  4. gh-ost 优于 pt-osc:在高并发或已有触发器的场景下,优先选择 gh-ost。
  5. 充分测试:在预发环境验证 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 支持哪些操作?

:主要包括:

  • 添加列(到表末尾);
  • 删除列(非末尾列需重建);
  • 设置/删除列默认值;
  • 修改枚举值列表(部分情况)。

注意:不能改变列顺序或数据类型。

相关推荐
爱学习的阿磊2 小时前
使用PyTorch构建你的第一个神经网络
jvm·数据库·python
惊讶的猫3 小时前
Redis双写一致性
数据库·redis·缓存
怣504 小时前
[特殊字符] MySQL数据表操作完全指南:增删改查的艺术
数据库·mysql·adb
安然无虞4 小时前
「MongoDB数据库」初见
数据库·mysql·mongodb
一起养小猫4 小时前
Flutter for OpenHarmony 实战:番茄钟应用完整开发指南
开发语言·jvm·数据库·flutter·信息可视化·harmonyos
Mr_Xuhhh4 小时前
MySQL视图详解:虚拟表的创建、使用与实战
数据库·mysql
AI_56784 小时前
MySQL索引优化全景指南:从慢查询诊断到智能调优
数据库·mysql
老虎06274 小时前
Redis入门,配置,常见面试题总结
数据库·redis·缓存
一起养小猫4 小时前
Flutter for OpenHarmony 实战:数据持久化方案深度解析
网络·jvm·数据库·flutter·游戏·harmonyos