在MySQL运维与业务开发中,千万级、亿级大表的表结构变更,始终是悬在研发与DBA头顶的达摩克利斯之剑。一次操作不慎,就可能引发锁表雪崩、主从延迟爆表、磁盘空间耗尽、甚至数据丢失等致命生产故障,无数线上服务都曾因大表DDL陷入长时间不可用。
一、MySQL大表DDL的核心痛点与底层执行逻辑
1.1 大表DDL的生产级核心痛点
大表DDL之所以成为生产故障的重灾区,核心源于其带来的四大不可控风险:
- 锁表与业务雪崩风险:不合理的DDL操作会长时间持有表级排他锁,阻塞所有读写请求,高并发场景下会瞬间导致连接数打满,服务完全不可用
- 性能与资源冲击:全表数据拷贝会打满磁盘IO与CPU,导致数据库性能骤降,正常业务请求响应超时,甚至引发上下游服务连锁故障
- 主从延迟失控:MySQL从库的SQL线程默认单线程执行,主库瞬间完成的DDL操作,传到从库会执行数小时,导致读写分离架构下读请求拿到过期数据,业务逻辑异常
- 数据与运维风险:DDL执行中途失败的回滚成本极高,甚至可能导致表空间损坏;大表拷贝需要1-2倍的磁盘空间,极易引发磁盘耗尽,导致数据库宕机
1.2 InnoDB DDL的三大执行模式全拆解
MySQL的DDL执行逻辑,本质是对表的元数据(数据字典)与物理存储结构的修改,核心分为三种执行模式,从旧到新、性能从差到优依次演进。
1.2.1 COPY模式(全表拷贝模式)
COPY模式是MySQL 5.5及之前版本的默认DDL执行方式,全程在Server层完成。其核心逻辑是:创建一张与原表结构完全一致的临时表,在临时表上执行目标DDL操作,然后逐行拷贝原表的全量数据到临时表,拷贝完成后原子性替换原表,最后删除临时表。
该模式的致命缺陷是全程持有表级排他锁,原表的所有DML操作完全阻塞,大表执行时间长达数小时,业务完全不可用;同时数据拷贝会产生双倍的磁盘IO开销,目前仅用于极少数不支持INPLACE的特殊DDL操作。
1.2.2 INPLACE模式(原地操作模式)
INPLACE模式是MySQL 5.6版本引入的核心特性,也是原生Online DDL的技术基础。这里需要纠正一个普遍误区:INPLACE并非完全不写入临时文件,而是所有操作都在InnoDB引擎层完成,无需Server层逐行拷贝数据,大幅减少了数据拷贝的开销,同时支持业务DML的并发执行。
INPLACE模式又分为两个子类型,开销差异极大:
- REBUILD模式:需要重建表的聚簇索引,创建全新的ibd表空间文件,完成全表数据的拷贝与重写。常见场景包括添加/修改主键、修改字段类型、修改表字符集、转换行格式等。该模式有一定的IO与CPU开销,但全程大部分时间支持DML并发执行。
- NO-REBUILD模式:无需重建表与拷贝数据,仅修改InnoDB的数据字典元信息,开销极小、执行速度极快。常见场景包括添加/删除普通二级索引、修改字段默认值、修改字段注释、修改索引类型等。
1.2.3 INSTANT模式(瞬时DDL模式)
INSTANT模式是MySQL 8.0.12版本推出的革命性升级,也是目前性能最优的DDL执行方式。其核心逻辑是仅修改数据字典中的元数据信息,完全不修改表的物理存储结构,不拷贝任何数据,不重建表,无论表的数据量是千万级还是亿级,都是毫秒级完成,全程不持有排他锁,业务DML操作完全不受影响。
目前MySQL 8.0.30+版本支持的INSTANT操作包括:
- 新增列(支持任意位置添加,无需强制放在表末尾)
- 修改列的默认值
- 修改列的注释
- 新增/删除分区(部分场景)
- 扩展VARCHAR类型的长度(字节数不超过255时)
- 修改索引类型
不支持INSTANT模式的操作,仍需降级为INPLACE或COPY模式执行,包括修改字段类型、修改字段的NULL/NOT NULL属性、删除列、修改表字符集等。
1.3 Online DDL的完整执行流程与MDL锁机制
原生Online DDL的核心,是通过两个极短的元数据锁(MDL)窗口期,替代了全程锁表,同时通过Row Log日志记录DDL执行期间的增量DML操作,最终重放增量保证数据一致性。其完整执行流程如下:
整个流程中,仅在准备阶段和提交阶段持有极短时间的MDL排他锁,其余时间仅持有MDL共享读锁,不阻塞业务的正常DML读写操作,这也是Online DDL的核心优势。
需要重点注意的是,这两个MDL排他锁的窗口期,必须等待该表上的所有长事务提交后才能获取。如果存在未提交的长事务持有该表的MDL共享锁,DDL的排他锁请求会进入等待队列,而后续所有对该表的读写请求,都会被DDL的排他锁请求阻塞,瞬间引发业务雪崩,这也是生产环境最常见的DDL故障。
二、三大在线DDL方案全维度拆解
针对大表DDL的生产需求,行业内形成了三大主流解决方案:MySQL原生Online DDL、Percona的pt-online-schema-change、GitHub开源的gh-ost。三者的底层实现逻辑完全不同,适用场景与优劣势也有显著差异。
2.1 原生Online DDL
原生Online DDL是MySQL官方内置的表结构变更方案,基于InnoDB引擎层的INPLACE/INSTANT模式实现,无需额外安装工具,是最基础、最常用的DDL方案。
核心执行逻辑
原生Online DDL在执行REBUILD模式操作时,会先创建一个临时的空表空间,按照新的表结构重建聚簇索引,逐批拷贝原表的历史数据到临时表空间;同时开启Row Log缓冲区,记录DDL执行期间所有对原表的DML增量操作。当历史数据全量拷贝完成后,会进入提交阶段,升级为MDL排他锁,重放Row Log中的所有增量数据,确保临时表与原表数据完全一致,最后原子性替换表空间文件,更新数据字典,清理临时文件与Row Log。
核心特性
- 原生内置,无需第三方依赖,使用门槛极低,仅需标准ALTER TABLE语法即可执行
- 支持INSTANT模式,毫秒级完成对应操作,无任何性能开销
- 操作具备原子性,执行失败会自动回滚,不会残留垃圾数据或影响原表结构
- 重建表的同时会清理表碎片,收缩表空间,优化表的查询性能
- 兼容所有InnoDB支持的表结构变更,兼容性最佳
优劣势与适用场景
优势:
- 官方原生支持,与MySQL版本完全兼容,无额外的运维成本
- INSTANT模式性能碾压所有第三方工具,无任何额外的性能开销
- 执行过程原子化,失败自动回滚,无需手动清理临时文件
- 支持所有类型的表结构变更,无语法限制
劣势:
- 执行过程不可控,一旦启动只能等待完成或手动终止,REBUILD模式下终止后需要全量回滚,时间成本极高
- REBUILD模式的IO开销不可控,无法动态限流,大表执行时极易打满服务器资源
- 主从架构下无延迟控制能力,主库执行完成后传到从库单线程执行,极易引发数小时的主从延迟
- 对MDL锁极其敏感,长事务会导致DDL锁等待,进而阻塞后续所有表操作,引发雪崩
- 不支持跨机房、跨实例的表结构同步
适用场景:
- MySQL 8.0.12+版本,支持INSTANT模式的表结构变更
- 千万行以下的中小表DDL操作
- 业务低峰期,可接受短暂性能影响的大表操作
- 无主从延迟敏感要求的业务场景
- 需要清理表碎片、收缩表空间的场景
2.2 pt-online-schema-change(pt-osc)
pt-online-schema-change是Percona Toolkit工具集的核心组件之一,是行业内最早成熟应用的开源在线DDL工具,基于触发器实现增量数据同步,彻底解决了原生DDL锁表的问题。
核心执行逻辑
pt-osc的核心是通过触发器实现增量数据的实时同步,将大表的全量数据拷贝拆分为多个小chunk分批执行,避免长时间锁表。其完整执行流程如下:
执行过程中,pt-osc会在原表上创建三个触发器,分别对应INSERT、UPDATE、DELETE操作:当原表有新的DML操作时,触发器会实时将变更同步到影子表,确保增量数据不丢失;同时按照预设的chunk大小,分批拷贝原表的历史数据到影子表,单chunk拷贝仅持有极短的行锁,不影响业务正常读写。全量数据拷贝完成后,会执行原子性的RENAME TABLE操作,将影子表替换为原表,最后清理旧表与触发器。
核心特性
- 全程不锁原表,所有DML操作均可正常执行,对业务的影响极小
- 支持分批拷贝,可配置chunk大小,精准控制IO与CPU开销
- 内置负载限流机制,当数据库负载超过预设阈值时,自动暂停数据拷贝,避免打满主库
- 支持主从延迟检测,当从库延迟超过阈值时自动暂停,保障读写分离架构的稳定性
- 执行中途可随时终止,仅需清理触发器与影子表即可,无回滚成本
优劣势与适用场景
优势:
- 完全解决了原生DDL锁表的问题,全程支持业务DML并发执行
- 可灵活配置限流规则,控制拷贝速度,避免影响线上业务稳定性
- 支持主从延迟自动检测与暂停,有效避免主从延迟失控
- 兼容MySQL 5.1+所有版本,对低版本数据库极其友好
- 执行过程可随时终止,无高额回滚成本
劣势:
- 基于触发器实现增量同步,会对原表的DML操作带来额外的性能开销,高并发场景下性能影响显著
- 触发器属于业务侵入式操作,需要在原表上创建触发器,存在触发器异常导致数据不一致的风险
- 对有外键约束的表支持较差,需要额外配置参数,处理逻辑复杂
- 不支持修改主键的表结构变更操作
- 原子切换阶段仍需要短暂的MDL排他锁,同样会被长事务阻塞
适用场景:
- MySQL 5.5/5.7等低版本数据库,不支持INSTANT DDL的场景
- 千万级大表,高并发业务,对锁表零容忍的场景
- 有主从延迟敏感要求,需要自动限流的业务场景
- 业务低峰期窗口较短,需要在业务高峰期执行DDL的场景
2.3 gh-ost
gh-ost是GitHub开源的无触发器在线DDL工具,全称为GitHub's Online Schema Transmogrifier,其核心创新是摒弃了触发器,通过模拟MySQL从库拉取并解析binlog实现增量数据同步,彻底解决了触发器带来的性能开销与风险,是目前亿级大表高并发场景的首选方案。
核心执行逻辑
gh-ost的核心是基于binlog的增量同步,无任何触发器侵入,将表结构变更的全流程解耦为历史数据拷贝与增量数据同步两个独立的流程,全程可控性极强。其完整执行流程如下:
gh-ost启动后,会先创建与原表结构一致的影子表,并在影子表上执行目标DDL操作;同时会模拟一个MySQL从库,连接到主库拉取binlog,实时解析原表的INSERT/UPDATE/DELETE事件,并异步应用到影子表,实现增量数据的同步。与此同时,gh-ost会按照预设的chunk大小,分批拷贝原表的历史数据到影子表,单chunk拷贝完成后会通过binlog补齐该窗口期的增量数据,确保数据一致性。当历史数据全量拷贝完成,且binlog增量完全追平后,会在业务最低峰期执行原子性的表名切换,完成DDL操作。
核心特性
- 无触发器设计,完全基于binlog实现增量同步,对原表的DML性能影响极小,高并发场景下优势显著
- 全程可控,支持随时暂停、恢复执行,支持动态调整chunk大小、限流阈值等参数,无需终止任务
- 支持多从库延迟检测,可配置多个从库的延迟阈值,任意从库延迟超标自动限流,保障主从架构稳定
- 无侵入式设计,不会在原表上创建任何触发器或其他对象,风险极低
- 支持测试模式,可在从库上模拟执行全流程,验证DDL逻辑与性能后再在主库执行
- 支持外键约束、主键修改等复杂DDL操作,兼容性远优于pt-osc
- 支持外挂式执行,即使gh-ost进程意外终止,也不会影响原表的正常业务
优劣势与适用场景
优势:
- 无触发器设计,对原库的性能影响远低于pt-osc与原生Online DDL,高并发核心业务场景优势极其明显
- 全程完全可控,支持随时暂停、恢复、动态调整参数,甚至修改DDL语句,灵活性拉满
- 主从延迟控制能力极强,可精准控制同步速度,彻底避免主从延迟失控
- 无业务侵入,不会修改原表的任何结构,故障风险极低
- 支持测试模式,可提前验证DDL逻辑,避免线上故障
- 支持主键修改、外键处理等复杂DDL操作,兼容性优秀
劣势:
- 强依赖ROW格式的binlog,不支持STATEMENT格式的binlog
- 需要额外安装工具,有一定的运维门槛
- 增量数据同步有毫秒级的延迟,切换阶段需要确保binlog完全追平
- 不支持MySQL 5.5及以下的极低版本
适用场景:
- 亿级大表,高并发核心业务,对性能影响零容忍的场景
- 主从延迟敏感的读写分离架构,需要精准控制延迟的场景
- 需要随时暂停、调整执行速度的灵活运维场景
- 修改主键、处理外键等复杂DDL操作场景
- 跨机房、跨实例的表结构同步场景
三、三大方案核心差异对比与选型指南
3.1 全维度对比表格
| 对比维度 | 原生Online DDL | pt-online-schema-change | gh-ost |
| 核心实现 | 引擎层INPLACE/INSTANT,Row Log记录增量 | 触发器同步增量,分批拷贝数据 | binlog解析同步增量,无触发器设计 |
| 锁表时长 | 两个极短的MDL排他锁窗口期 | 仅原子切换阶段极短锁表 | 仅原子切换阶段极短锁表 |
| 原库性能影响 | INSTANT模式无影响;REBUILD模式中等 | 高并发场景下影响较大(触发器额外开销) | 极小,远低于前两者 |
| 主从延迟控制 | 无控制能力,极易引发从库延迟 | 支持单从库延迟检测,自动暂停 | 支持多从库精准控制,自动限流 |
| 执行可控性 | 不可控,中途只能终止,回滚成本极高 | 支持中途终止,无回滚成本 | 完全可控,支持暂停/恢复/动态调参 |
| 失败回滚成本 | 极高,REBUILD模式需要全量回滚 | 低,删除触发器和影子表即可 | 极低,仅需删除影子表即可 |
| 业务侵入性 | 无侵入,原生SQL操作 | 侵入式,需在原表创建触发器 | 无侵入,不修改原表任何结构 |
| 复杂DDL支持 | 全支持,兼容性最好 | 不支持主键修改,外键处理复杂 | 支持主键修改、外键,兼容性优秀 |
| MySQL版本要求 | 5.6+支持Online,8.0.12+支持INSTANT | 5.1+全版本支持 | 5.6+,要求binlog_format=ROW |
| 运维成本 | 极低,原生SQL即可执行 | 中等,需安装工具,配置参数 | 中等,需安装工具,配置灵活度高 |
| 大表支持能力 | 亿级以上大表REBUILD模式风险极高 | 支持亿级大表,高并发场景有瓶颈 | 完美支持十亿级大表,高并发场景无压力 |
3.2 分场景选型黄金法则
基于三大方案的特性差异,生产环境的选型需严格遵循以下黄金法则,最大化规避风险:
- 优先选型:MySQL 8.0.12+版本,目标DDL操作支持INSTANT模式时,优先使用原生Online DDL,毫秒级完成,无任何性能风险,是最优解。
- 次优先选型:MySQL 5.7/8.0版本,不支持INSTANT模式的DDL操作,亿级大表、高并发核心业务,优先使用gh-ost,性能影响最小,可控性最强,风险最低。
- 兼容选型:MySQL 5.5/5.6低版本,无法配置ROW格式binlog的场景,使用pt-osc,兼容性最好,可满足在线DDL的核心需求。
- 特殊场景选型:
- 需要收缩表空间、清理表碎片:优先使用原生Online DDL REBUILD模式,或gh-ost
- 修改主键、处理外键等复杂DDL:优先使用gh-ost,其次原生Online DDL
- 中小表、业务低峰期的简单DDL:原生Online DDL,最简单高效
- 跨机房、跨实例的表结构同步:优先使用gh-ost
四、生产级大表DDL避坑全指南
4.1 事前:90%故障的前置规避
绝大多数DDL生产故障,都可以通过事前的充分校验与准备规避,核心做好以下4件事:
1. 操作与版本评估
- 先确认MySQL版本,明确目标DDL操作是否支持INSTANT模式,优先使用INSTANT模式执行
- 评估DDL操作的类型,明确是NO-REBUILD、REBUILD还是COPY模式,预估执行时间与资源开销
- 禁止在业务高峰期执行REBUILD/COPY模式的DDL操作,必须选择业务低峰期(凌晨流量最低的窗口)执行
- 禁止一次性执行多个DDL操作,必须拆分为多个单条DDL依次执行,减少锁表时间与资源开销
2. 资源与环境前置校验
- 磁盘空间校验:必须确保数据库服务器的磁盘剩余空间,至少是目标表大小的1.2倍以上;REBUILD模式会创建临时表空间,同时binlog会产生大量日志,极易引发磁盘耗尽
- 表大小与碎片评估:提前查询目标表的实际数据量、行数与碎片率,碎片率超过30%的表,执行DDL的同时可完成碎片清理
- binlog格式校验:使用gh-ost必须提前确认数据库的binlog_format=ROW,binlog_row_image=FULL,并在配置文件中永久生效
- 主从架构校验:确认主从同步状态正常,从库延迟为0,提前规划好延迟检测的阈值与从库节点
3. 致命风险前置排查:长事务与MDL锁
长事务导致的MDL锁等待,是生产环境最常见的DDL雪崩故障,必须在执行前彻底排查:
- 提前查询目标表上是否有执行超过30秒的长事务,长事务会持有该表的MDL共享锁,导致DDL的排他锁请求无法获取,进而阻塞后续所有对该表的操作
- 处理方案:DDL执行前,必须kill掉目标表上的长事务,或等待长事务执行完成,绝对不能在有长事务的情况下执行DDL
- 进阶防护:执行DDL时设置lock_wait_timeout参数,避免DDL长时间等待MDL锁,引发雪崩
4. 测试与预案准备
- 先在测试环境执行一遍目标DDL,验证语法正确性、执行时间、性能影响,确保无语法错误
- 制定完整的回滚预案,明确故障发生时的终止操作、临时文件清理、业务恢复流程
- 核心业务表的大表DDL,先在从库上执行一遍,验证执行时间与性能影响,再在主库执行
4.2 事中:执行过程的精准管控
1. 原生Online DDL执行管控
- 必须显式指定ALGORITHM和LOCK参数,避免MySQL自动降级为COPY模式,例如
ALTER TABLE t ADD INDEX idx_name(name), ALGORITHM=INPLACE, LOCK=NONE; - 大表REBUILD模式执行前,调大
innodb_online_alter_log_max_size参数,避免Row Log溢出导致DDL执行失败,建议设置为4G以上 - 执行期间持续监控服务器的CPU、IO、内存使用率,以及数据库的连接数、QPS、TPS,出现异常及时处理
- 持续监控MDL锁等待情况,出现长时间锁等待时,及时终止DDL操作,避免业务雪崩
2. pt-osc执行管控
- 必须先加
--dry-run参数执行,验证语法与逻辑的正确性,无问题后再加--execute参数正式执行 - 配置合理的
--chunk-size,默认1000行,根据表的行大小调整,避免单chunk拷贝时间过长 - 配置
--max-load与--critical-load参数,控制数据库的负载阈值,超过阈值自动暂停或终止执行,避免打满数据库 - 配置
--check-slave-lag与--max-lag参数,监控从库延迟,延迟超过阈值自动暂停,避免主从延迟失控 - 有外键的表执行时,必须正确配置
--alter-foreign-keys-method参数,妥善处理外键约束
3. gh-ost执行管控
- 先加
--test-on-replica参数,在从库上模拟执行全流程,验证DDL逻辑与性能,无问题后再在主库正式执行 - 配置
--max-load与--critical-load参数,控制主库的负载阈值 - 配置
--throttle-control-replicas参数,指定所有从库节点,设置--max-lag-millis阈值,任意从库延迟超标自动限流 - 开启
--allow-on-master参数,允许直接在主库执行 - 执行期间可通过socket文件动态调整参数,无需终止执行,出现异常可随时暂停,排查问题后恢复执行
- 表名切换阶段,选择业务流量最低的时间点执行,确保切换过程无业务影响
4.3 事后:收尾校验与风险排查
- 数据一致性校验:执行完成后,校验表的行数、核心字段的数据完整性,确保没有数据丢失;第三方工具执行的DDL,需校验原表与新表的数据一致性
- 表结构与索引校验:校验新增的字段、索引是否生效,表结构是否符合预期,索引是否可正常命中
- 主从同步校验:检查所有从库的同步状态,确保主从延迟恢复到0,无同步错误;校验从库的表结构、索引是否与主库完全一致
- 业务与性能校验:检查数据库的CPU、IO、内存使用率是否恢复正常;校验业务的读写接口是否正常,有无新增慢查询,新增的索引是否正常命中
- 垃圾数据清理:清理执行过程中产生的旧表、临时表、触发器等垃圾数据,释放磁盘空间
4.4 十大致命坑与规避方案
- 长事务导致MDL锁等待,引发业务雪崩
- 现象:DDL执行后,瞬间所有对该表的查询都被阻塞,连接数飙升,业务宕机
- 根因:长事务持有目标表的MDL共享锁,DDL的排他锁请求进入等待队列,后续所有对该表的操作都会被DDL的请求阻塞
- 规避:DDL前必须kill所有目标表上的长事务;设置lock_wait_timeout=30,避免DDL长时间等待;业务代码禁止使用长事务
- 磁盘空间不足,DDL中途失败导致数据库宕机
- 现象:大表DDL执行到一半,磁盘空间耗尽,DDL失败,MySQL无法写入binlog,服务宕机
- 根因:未提前评估磁盘空间,REBUILD模式需要1.2倍以上的表空间,同时binlog会产生大量日志
- 规避:DDL前必须校验磁盘空间,确保剩余空间是表大小的1.5倍以上;大表DDL期间,调整binlog过期时间,加速旧binlog清理
- 主库DDL完成,从库延迟爆表,读写分离失效
- 现象:主库DDL执行完成,业务正常,但从库延迟数小时,读请求拿到过期数据,业务逻辑异常
- 根因:MySQL从库的SQL线程默认单线程执行,主库并行执行的DDL,传到从库只能单线程执行,大表DDL会导致从库长时间阻塞
- 规避:优先使用gh-ost/pt-osc,支持主从延迟自动限流;从库开启多线程复制(slave_parallel_type=LOGICAL_CLOCK,slave_parallel_workers=8);大表DDL拆分多个小操作执行
- Online DDL的Row Log溢出,执行失败
- 现象:大表DDL执行数小时后,报错失败,所有工作白费
- 根因:DDL执行期间,业务DML量极大,Row Log的大小超过了innodb_online_alter_log_max_size的设置,导致DDL执行失败
- 规避:大表DDL前,将innodb_online_alter_log_max_size设置为4G以上;必须在业务低峰期执行;DML量极大的表,优先使用gh-ost
- 修改VARCHAR长度,误触发全表重建
- 现象:本以为是小操作,将VARCHAR(20)改为VARCHAR(30),结果触发全表重建,业务阻塞
- 根因:MySQL 5.7中,VARCHAR长度修改超过255字节,或从大改小,都会触发全表重建;8.0版本中,仅字节数不超过255的长度增加,才支持INSTANT模式
- 规避:修改VARCHAR长度前,先确认版本与支持的模式;显式指定ALGORITHM=INSTANT,不支持会直接报错,不会自动降级;表设计阶段提前规划好字段长度,避免随意修改
- 大表加唯一索引,重复值导致DDL失败
- 现象:大表加唯一索引,执行数小时后,报错存在重复值,执行失败
- 根因:加唯一索引时,MySQL会全表校验数据唯一性,存在重复值就会执行失败,且中途无法暂停
- 规避:加唯一索引前,先通过SQL查询重复值,提前清理;优先使用gh-ost,可提前校验重复值;大表加唯一索引,拆分成分批去重+加索引两步执行
- pt-osc触发器异常,导致数据丢失
- 现象:pt-osc执行中途,触发器异常,增量数据未同步到影子表,切换后数据丢失
- 根因:原表已存在触发器,与pt-osc的触发器冲突;或数据库异常重启,触发器失效
- 规避:pt-osc执行前,先检查原表是否存在触发器,提前处理;核心业务表优先使用gh-ost,无触发器风险;执行期间持续监控触发器状态
- gh-ost binlog格式错误,执行失败
- 现象:gh-ost执行报错,无法解析binlog
- 根因:数据库的binlog_format=STATEMENT,gh-ost仅支持ROW格式的binlog
- 规避:gh-ost执行前,先确认binlog_format=ROW,binlog_row_image=FULL;在my.cnf中永久配置,避免重启后失效
- DDL中途数据库宕机,表空间损坏
- 现象:DDL执行中途,服务器宕机,重启后表损坏,无法访问
- 根因:COPY模式的DDL中途宕机,临时表不完整,原表也可能损坏;原生Online DDL的REBUILD模式中途宕机,回滚成本极高
- 规避:核心业务表优先使用gh-ost/pt-osc,中途宕机不会影响原表;禁止在业务高峰期执行DDL;执行前做好数据库全量备份
- 外键表DDL,导致数据不一致
- 现象:有外键的表执行DDL后,外键约束失效,数据不一致
- 根因:pt-osc执行RENAME表后,外键的关联关系指向了旧表,导致外键失效;原生Online DDL修改外键关联字段,会导致外键约束异常
- 规避:有外键的表,优先使用gh-ost,对外键的支持更好;pt-osc执行外键表,必须正确配置--alter-foreign-keys-method参数;DDL执行后,必须校验外键约束是否生效
五、示例
5.1 原生Online DDL实战SQL
示例环境:MySQL 8.0.36,表user_info,数据量1000万行
-- 1. 瞬时加字段(INSTANT模式,毫秒级完成)
ALTER TABLE user_info
ADD COLUMN user_level TINYINT NOT NULL DEFAULT 1 COMMENT '用户等级',
ALGORITHM=INSTANT, LOCK=NONE;
-- 2. 加普通二级索引(INPLACE NO-REBUILD模式,不锁表)
ALTER TABLE user_info
ADD INDEX idx_create_time (create_time),
ALGORITHM=INPLACE, LOCK=NONE;
-- 3. 加联合索引(INPLACE NO-REBUILD模式)
ALTER TABLE user_info
ADD INDEX idx_phone_status (phone, user_status),
ALGORITHM=INPLACE, LOCK=NONE;
-- 4. 修改字段类型(INPLACE REBUILD模式,大表需在低峰期执行)
ALTER TABLE user_info
MODIFY COLUMN phone VARCHAR(20) NOT NULL COMMENT '用户手机号',
ALGORITHM=INPLACE, LOCK=NONE;
-- 5. 修改字段默认值(INSTANT模式)
ALTER TABLE user_info
ALTER COLUMN user_status SET DEFAULT 0 COMMENT '用户状态默认值',
ALGORITHM=INSTANT, LOCK=NONE;
-- 6. 长事务排查SQL(DDL前必须执行)
SELECT
trx_id,
trx_started,
TIME_TO_SEC(TIMEDIFF(NOW(),trx_started)) AS trx_duration_sec,
trx_mysql_thread_id,
trx_query
FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(),trx_started)) > 30
ORDER BY trx_duration_sec DESC;
-- 7. MDL锁等待排查SQL
SELECT
waiting_pid,
waiting_query,
waiting_lock_type,
blocking_pid,
blocking_query,
blocking_lock_type
FROM sys.schema_table_lock_waits;
-- 8. 表大小与碎片率查询SQL
SELECT
table_schema,
table_name,
CONCAT(ROUND(table_rows/10000,2),'万行') AS table_rows,
CONCAT(ROUND(data_length/1024/1024/1024,2),'GB') AS data_size,
CONCAT(ROUND(index_length/1024/1024/1024,2),'GB') AS index_size,
CONCAT(ROUND(data_free/1024/1024/1024,2),'GB') AS free_size,
ROUND(data_free/(data_length+index_length)*100,2) AS fragment_rate
FROM information_schema.TABLES
WHERE table_schema NOT IN ('information_schema','mysql','performance_schema','sys')
ORDER BY (data_length+index_length) DESC;
5.2 pt-osc生产实战命令
示例环境:Percona Toolkit 3.5.7,MySQL 5.7.44,表order_info,数据量5000万行
# 1. 预执行(dry-run),验证语法和逻辑,不会修改任何数据
pt-online-schema-change \
--alter="ADD COLUMN order_source TINYINT NOT NULL DEFAULT 0 COMMENT '订单来源'" \
--user=root \
--password=your_secure_password \
--host=127.0.0.1 \
--port=3306 \
--charset=utf8mb4 \
--dry-run \
D=trade_db,t=order_info
# 2. 正式执行,生产环境最佳实践参数
pt-online-schema-change \
--alter="ADD COLUMN order_source TINYINT NOT NULL DEFAULT 0 COMMENT '订单来源'" \
--user=root \
--password=your_secure_password \
--host=127.0.0.1 \
--port=3306 \
--charset=utf8mb4 \
--chunk-size=1000 \
--max-load Threads_running=50 \
--critical-load Threads_running=100 \
--check-slave-lag h=192.168.1.100,P=3306,u=root,p=your_secure_password \
--max-lag=1 \
--check-interval=5 \
--progress=time,30 \
--execute \
D=trade_db,t=order_info
5.3 gh-ost生产实战命令
示例环境:gh-ost 1.1.6,MySQL 8.0.36,binlog_format=ROW,表pay_info,数据量1.2亿行
# 1. 从库测试执行,验证逻辑,不会修改主库数据
gh-ost \
--user=root \
--password=your_secure_password \
--host=192.168.1.100 \
--port=3306 \
--database=pay_db \
--table=pay_info \
--alter="ADD COLUMN pay_channel TINYINT NOT NULL DEFAULT 0 COMMENT '支付渠道'" \
--test-on-replica \
--verbose
# 2. 主库正式执行,生产环境最佳实践参数
gh-ost \
--user=root \
--password=your_secure_password \
--host=127.0.0.1 \
--port=3306 \
--database=pay_db \
--table=pay_info \
--alter="ADD COLUMN pay_channel TINYINT NOT NULL DEFAULT 0 COMMENT '支付渠道'" \
--allow-on-master \
--chunk-size=1000 \
--max-load Threads_running=50 \
--critical-load Threads_running=100 \
--throttle-control-replicas="192.168.1.100:3306,192.168.1.101:3306" \
--max-lag-millis=1000 \
--throttle-query="select @@global.read_only" \
--initially-drop-ghost-table \
--initially-drop-old-table \
--exact-rowcount \
--progress-status-interval-seconds=30 \
--execute
5.4 Java DDL运维监控服务全实现
pom.xml 依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>mysql-ddl-ops</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mysql-ddl-ops</name>
<description>MySQL大表DDL运维监控服务</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<springdoc.version>2.5.0</springdoc.version>
<lombok.version>1.18.32</lombok.version>
<fastjson2.version>2.0.52</fastjson2.version>
<guava.version>33.1.0-jre</guava.version>
<mysql.version>8.0.37</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml 配置文件
spring:
application:
name: mysql-ddl-ops
datasource:
url: jdbc:mysql://127.0.0.1:3306/test_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: your_secure_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
swagger-ui:
path: /swagger-ui.html
enabled: true
api-docs:
enabled: true
path: /v3/api-docs
server:
port: 8080
实体类
package com.jam.demo.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "长事务信息")
public class LongTransactionInfo {
@Schema(description = "事务ID")
private String trxId;
@Schema(description = "事务开始时间")
private LocalDateTime trxStarted;
@Schema(description = "事务持续时长(秒)")
private Long trxDurationSec;
@Schema(description = "事务线程ID")
private Long trxMysqlThreadId;
@Schema(description = "事务执行的SQL")
private String trxQuery;
}
package com.jam.demo.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(description = "表信息")
public class TableInfo {
@Schema(description = "数据库名")
private String tableSchema;
@Schema(description = "表名")
private String tableName;
@Schema(description = "表行数")
private String tableRows;
@Schema(description = "数据大小")
private String dataSize;
@Schema(description = "索引大小")
private String indexSize;
@Schema(description = "空闲空间")
private String freeSize;
@Schema(description = "碎片率(%)")
private BigDecimal fragmentRate;
}
package com.jam.demo.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "MDL锁等待信息")
public class MdlLockWaitInfo {
@Schema(description = "等待线程ID")
private Long waitingPid;
@Schema(description = "等待执行的SQL")
private String waitingQuery;
@Schema(description = "等待的锁类型")
private String waitingLockType;
@Schema(description = "阻塞线程ID")
private Long blockingPid;
@Schema(description = "阻塞线程执行的SQL")
private String blockingQuery;
@Schema(description = "持有的锁类型")
private String blockingLockType;
}
package com.jam.demo.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "主从延迟信息")
public class ReplicationDelayInfo {
@Schema(description = "从库地址")
private String slaveHost;
@Schema(description = "主从延迟(秒)")
private Long delaySeconds;
@Schema(description = "IO线程状态")
private String ioState;
@Schema(description = "SQL线程状态")
private String sqlState;
@Schema(description = "主库地址")
private String masterHost;
}
Mapper层
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.LongTransactionInfo;
import com.jam.demo.entity.MdlLockWaitInfo;
import com.jam.demo.entity.ReplicationDelayInfo;
import com.jam.demo.entity.TableInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface DdlOpsMapper extends BaseMapper<Object> {
List<LongTransactionInfo> selectLongTransaction(@Param("minDurationSec") Long minDurationSec);
List<MdlLockWaitInfo> selectMdlLockWait();
List<TableInfo> selectAllTableInfo();
TableInfo selectTableInfo(@Param("tableSchema") String tableSchema, @Param("tableName") String tableName);
ReplicationDelayInfo selectReplicationDelay();
void killThread(@Param("threadId") Long threadId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.DdlOpsMapper">
<select id="selectLongTransaction" resultType="com.jam.demo.entity.LongTransactionInfo">
SELECT
trx_id AS trxId,
trx_started AS trxStarted,
TIME_TO_SEC(TIMEDIFF(NOW(),trx_started)) AS trxDurationSec,
trx_mysql_thread_id AS trxMysqlThreadId,
trx_query AS trxQuery
FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(),trx_started)) > #{minDurationSec}
ORDER BY trxDurationSec DESC
</select>
<select id="selectMdlLockWait" resultType="com.jam.demo.entity.MdlLockWaitInfo">
SELECT
waiting_pid AS waitingPid,
waiting_query AS waitingQuery,
waiting_lock_type AS waitingLockType,
blocking_pid AS blockingPid,
blocking_query AS blockingQuery,
blocking_lock_type AS blockingLockType
FROM sys.schema_table_lock_waits
</select>
<select id="selectAllTableInfo" resultType="com.jam.demo.entity.TableInfo">
SELECT
table_schema AS tableSchema,
table_name AS tableName,
CONCAT(ROUND(table_rows/10000,2),'万行') AS tableRows,
CONCAT(ROUND(data_length/1024/1024/1024,2),'GB') AS dataSize,
CONCAT(ROUND(index_length/1024/1024/1024,2),'GB') AS indexSize,
CONCAT(ROUND(data_free/1024/1024/1024,2),'GB') AS freeSize,
ROUND(data_free/(data_length+index_length)*100,2) AS fragmentRate
FROM information_schema.TABLES
WHERE table_schema NOT IN ('information_schema','mysql','performance_schema','sys')
ORDER BY (data_length+index_length) DESC
</select>
<select id="selectTableInfo" resultType="com.jam.demo.entity.TableInfo">
SELECT
table_schema AS tableSchema,
table_name AS tableName,
CONCAT(ROUND(table_rows/10000,2),'万行') AS tableRows,
CONCAT(ROUND(data_length/1024/1024/1024,2),'GB') AS dataSize,
CONCAT(ROUND(index_length/1024/1024/1024,2),'GB') AS indexSize,
CONCAT(ROUND(data_free/1024/1024/1024,2),'GB') AS freeSize,
ROUND(data_free/(data_length+index_length)*100,2) AS fragmentRate
FROM information_schema.TABLES
WHERE table_schema = #{tableSchema}
AND table_name = #{tableName}
</select>
<select id="selectReplicationDelay" resultType="com.jam.demo.entity.ReplicationDelayInfo">
SHOW SLAVE STATUS
</select>
<update id="killThread">
KILL #{threadId}
</update>
</mapper>
Service层
package com.jam.demo.service;
import com.jam.demo.entity.LongTransactionInfo;
import com.jam.demo.entity.MdlLockWaitInfo;
import com.jam.demo.entity.ReplicationDelayInfo;
import com.jam.demo.entity.TableInfo;
import java.util.List;
public interface DdlOpsService {
List<LongTransactionInfo> getLongTransactionList(Long minDurationSec);
List<MdlLockWaitInfo> getMdlLockWaitList();
List<TableInfo> getAllTableInfoList();
TableInfo getTableInfo(String tableSchema, String tableName);
ReplicationDelayInfo getReplicationDelayInfo();
void killLongTransaction(Long threadId);
String ddlPreCheck(String tableSchema, String tableName);
}
package com.jam.demo.service.impl;
import com.jam.demo.entity.LongTransactionInfo;
import com.jam.demo.entity.MdlLockWaitInfo;
import com.jam.demo.entity.ReplicationDelayInfo;
import com.jam.demo.entity.TableInfo;
import com.jam.demo.mapper.DdlOpsMapper;
import com.jam.demo.service.DdlOpsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class DdlOpsServiceImpl implements DdlOpsService {
private final DdlOpsMapper ddlOpsMapper;
private final PlatformTransactionManager transactionManager;
@Override
public List<LongTransactionInfo> getLongTransactionList(Long minDurationSec) {
if (ObjectUtils.isEmpty(minDurationSec) || minDurationSec < 0) {
minDurationSec = 30L;
}
return ddlOpsMapper.selectLongTransaction(minDurationSec);
}
@Override
public List<MdlLockWaitInfo> getMdlLockWaitList() {
return ddlOpsMapper.selectMdlLockWait();
}
@Override
public List<TableInfo> getAllTableInfoList() {
return ddlOpsMapper.selectAllTableInfo();
}
@Override
public TableInfo getTableInfo(String tableSchema, String tableName) {
if (!StringUtils.hasText(tableSchema) || !StringUtils.hasText(tableName)) {
throw new IllegalArgumentException("数据库名和表名不能为空");
}
return ddlOpsMapper.selectTableInfo(tableSchema, tableName);
}
@Override
public ReplicationDelayInfo getReplicationDelayInfo() {
return ddlOpsMapper.selectReplicationDelay();
}
@Override
public void killLongTransaction(Long threadId) {
if (ObjectUtils.isEmpty(threadId) || threadId <= 0) {
throw new IllegalArgumentException("线程ID不能为空且必须大于0");
}
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
ddlOpsMapper.killThread(threadId);
transactionManager.commit(status);
log.info("成功kill长事务线程, threadId:{}", threadId);
} catch (Exception e) {
transactionManager.rollback(status);
log.error("kill长事务线程失败, threadId:{}", threadId, e);
throw new RuntimeException("kill长事务线程失败", e);
}
}
@Override
public String ddlPreCheck(String tableSchema, String tableName) {
StringBuilder checkResult = new StringBuilder();
checkResult.append("===== DDL执行前置校验结果 =====\n");
TableInfo tableInfo = getTableInfo(tableSchema, tableName);
if (ObjectUtils.isEmpty(tableInfo)) {
checkResult.append("❌ 表不存在: ").append(tableSchema).append(".").append(tableName).append("\n");
return checkResult.toString();
}
checkResult.append("✅ 表存在: ").append(tableSchema).append(".").append(tableName).append("\n");
checkResult.append("📊 表行数: ").append(tableInfo.getTableRows()).append("\n");
checkResult.append("📊 数据大小: ").append(tableInfo.getDataSize()).append("\n");
checkResult.append("📊 索引大小: ").append(tableInfo.getIndexSize()).append("\n");
checkResult.append("📊 碎片率: ").append(tableInfo.getFragmentRate()).append("%\n");
List<LongTransactionInfo> longTransactionList = getLongTransactionList(30L);
if (!CollectionUtils.isEmpty(longTransactionList)) {
checkResult.append("❌ 发现").append(longTransactionList.size()).append("个长事务, 可能导致MDL锁等待\n");
for (LongTransactionInfo transaction : longTransactionList) {
checkResult.append(" - 线程ID: ").append(transaction.getTrxMysqlThreadId())
.append(", 持续时长: ").append(transaction.getTrxDurationSec()).append("秒\n");
}
} else {
checkResult.append("✅ 未发现长事务\n");
}
List<MdlLockWaitInfo> mdlLockWaitList = getMdlLockWaitList();
if (!CollectionUtils.isEmpty(mdlLockWaitList)) {
checkResult.append("❌ 发现").append(mdlLockWaitList.size()).append("个MDL锁等待\n");
} else {
checkResult.append("✅ 未发现MDL锁等待\n");
}
ReplicationDelayInfo replicationDelayInfo = getReplicationDelayInfo();
if (!ObjectUtils.isEmpty(replicationDelayInfo)) {
Long delaySeconds = replicationDelayInfo.getDelaySeconds();
if (!ObjectUtils.isEmpty(delaySeconds) && delaySeconds > 0) {
checkResult.append("⚠️ 当前主从延迟: ").append(delaySeconds).append("秒\n");
} else {
checkResult.append("✅ 主从同步正常, 延迟为0\n");
}
} else {
checkResult.append("⚠️ 未检测到主从同步配置\n");
}
checkResult.append("===== 校验完成 =====\n");
log.info("DDL前置校验完成, table:{}.{}, result:{}", tableSchema, tableName, checkResult);
return checkResult.toString();
}
}
Controller层
package com.jam.demo.controller;
import com.jam.demo.entity.LongTransactionInfo;
import com.jam.demo.entity.MdlLockWaitInfo;
import com.jam.demo.entity.ReplicationDelayInfo;
import com.jam.demo.entity.TableInfo;
import com.jam.demo.service.DdlOpsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/ddl/ops")
@RequiredArgsConstructor
@Tag(name = "DDL运维监控接口", description = "MySQL大表DDL执行前的校验、监控与运维接口")
public class DdlOpsController {
private final DdlOpsService ddlOpsService;
@GetMapping("/long-transaction")
@Operation(summary = "查询长事务列表", description = "查询执行时长超过指定秒数的长事务列表")
public ResponseEntity<List<LongTransactionInfo>> getLongTransactionList(
@Parameter(description = "最小持续时长(秒)", example = "30")
@RequestParam(required = false, defaultValue = "30") Long minDurationSec) {
return ResponseEntity.ok(ddlOpsService.getLongTransactionList(minDurationSec));
}
@GetMapping("/mdl-lock-wait")
@Operation(summary = "查询MDL锁等待列表", description = "查询当前数据库中的MDL锁等待信息")
public ResponseEntity<List<MdlLockWaitInfo>> getMdlLockWaitList() {
return ResponseEntity.ok(ddlOpsService.getMdlLockWaitList());
}
@GetMapping("/table-info/all")
@Operation(summary = "查询所有表信息", description = "查询数据库中所有业务表的大小、行数、碎片率等信息")
public ResponseEntity<List<TableInfo>> getAllTableInfoList() {
return ResponseEntity.ok(ddlOpsService.getAllTableInfoList());
}
@GetMapping("/table-info")
@Operation(summary = "查询指定表信息", description = "查询指定表的大小、行数、碎片率等详细信息")
public ResponseEntity<TableInfo> getTableInfo(
@Parameter(description = "数据库名", required = true, example = "test_db")
@RequestParam String tableSchema,
@Parameter(description = "表名", required = true, example = "user_info")
@RequestParam String tableName) {
return ResponseEntity.ok(ddlOpsService.getTableInfo(tableSchema, tableName));
}
@GetMapping("/replication-delay")
@Operation(summary = "查询主从延迟信息", description = "查询当前从库的主从同步延迟信息")
public ResponseEntity<ReplicationDelayInfo> getReplicationDelayInfo() {
return ResponseEntity.ok(ddlOpsService.getReplicationDelayInfo());
}
@PostMapping("/kill-transaction")
@Operation(summary = "kill长事务线程", description = "终止指定的长事务线程,避免MDL锁等待")
public ResponseEntity<String> killLongTransaction(
@Parameter(description = "事务线程ID", required = true, example = "12345")
@RequestParam Long threadId) {
ddlOpsService.killLongTransaction(threadId);
return ResponseEntity.ok("成功kill长事务线程: " + threadId);
}
@GetMapping("/pre-check")
@Operation(summary = "DDL执行前置校验", description = "执行DDL前的全量校验,包括表信息、长事务、MDL锁、主从延迟等")
public ResponseEntity<String> ddlPreCheck(
@Parameter(description = "数据库名", required = true, example = "test_db")
@RequestParam String tableSchema,
@Parameter(description = "表名", required = true, example = "user_info")
@RequestParam String tableName) {
return ResponseEntity.ok(ddlOpsService.ddlPreCheck(tableSchema, tableName));
}
}
启动类
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class MysqlDdlOpsApplication {
public static void main(String[] args) {
SpringApplication.run(MysqlDdlOpsApplication.class, args);
}
}
总结
MySQL大表DDL的核心,是在表结构变更的业务需求与数据库稳定性之间找到最优平衡。理解不同DDL方案的底层实现逻辑,是规避故障的核心前提。在生产实践中,始终遵循“能INSTANT不INPLACE,能INPLACE不COPY,大表高并发优先gh-ost,低版本兼容优先pt-osc”的核心原则,同时做好事前校验、事中管控、事后校验,就能彻底规避大表DDL带来的生产故障,实现零宕机的表结构变更。