当前位置: 代码网 > it编程>数据库>Mysql > MySQL数据库大表DDL变更操作的原理、方案与实践教学

MySQL数据库大表DDL变更操作的原理、方案与实践教学

2026年08月23日 Mysql 我要评论
一、什么是 ddl 变更1.1 基本概念术语全称含义示例ddldata definition language数据定义语言,修改表结构alter table / create table / drop

一、什么是 ddl 变更

1.1 基本概念

术语全称含义示例
ddldata definition language数据定义语言,修改表结构alter table / create table / drop table
dmldata manipulation language数据操作语言,操作数据insert / update / delete / select 
mdlmetadata lock元数据锁,保护表结构不被并发修改ddl 执行时自动获取

1.2 为什么大表 ddl 是难题

  • 小表(几万行):alter table → 秒级完成,无感知
  • 中表(几百万行):alter table → 几十秒到几分钟,可接受
  • 大表(几千万行):alter table → 几十分钟到几小时,线上不可接受

核心矛盾:ddl 执行时间 与 业务可接受的中断时间 不匹配

二、mysql ddl 的底层原理

2.1 mysql 5.6 之前(copy 方式)

alter table orders add column remark varchar(255);

执行过程:

  • 获取表的 mdl 独占锁(阻塞所有读写!)
  • 创建一张临时表(包含新字段)
  • 逐行复制原表数据到临时表
  • 删除原表
  • 临时表改名为原表名
  • 释放 mdl 锁

问题:整个过程表不可读写,4000万行复制可能需要30分钟+

2.2 mysql 5.6+ online ddl(inplace 方式)

alter table orders add column remark varchar(255), algorithm=inplace;

执行过程:

  • 获取 mdl 共享锁(允许读写)
  • 在引擎层原地修改表结构(不复制全部数据)
  • 期间记录增量变更到 online ddl log
  • 结束时短暂升级为 mdl 独占锁(通常毫秒级)
  • 应用增量,释放锁

改善:执行期间允许 dml 操作(读写不阻塞)

但是:仍然需要重建表(取决于操作类型),耗时仍然长

2.3 mysql 8.0.12+ instant ddl

alter table orders add column remark varchar(255), algorithm=instant;

执行过程:

  • 只修改数据字典中的表定义(元数据)
  • 不重建表、不复制数据
  • 瞬间完成(毫秒级)

限制:

  • 只支持在表末尾加列(8.0.29+ 支持任意位置)
  • 新列必须允许 null 或有默认值
  • 不支持加索引、改列类型等操作

2.4 各版本 ddl 能力对比

操作5.6 之前5.6/5.7 online ddl8.0.12+ instant
加可空列copy(锁表)inplace(不锁表但重建)instant(秒完成)
加索引copyinplace(不锁表但耗时)inplace
改列类型copycopy(锁表!)copy
删列copyinplaceinplace
改表名瞬间瞬间瞬间

三、mdl 锁详解

3.1 什么是 mdl 锁

mdl(metadata lock)是 mysql 5.5 引入的机制,保证 ddl 和 dml 不冲突:

  • dml 操作(select/insert/update/delete)→ 获取 mdl 读锁(共享锁)
  • ddl 操作(alter table)→ 需要 mdl 写锁(独占锁)
  • mdl 读锁之间不互斥 → 多个 dml 可以并发执行
  • mdl 写锁与任何锁互斥 → ddl 必须等所有 dml 释放读锁后才能获取写锁

3.2 mdl 锁导致的雪崩

  • 时刻t1:事务a执行 select(持有mdl读锁,事务未提交)
  • 时刻t2:dba执行 alter table(等待mdl写锁)
  • 时刻t3:新来的事务b执行 select → 被mdl写锁请求阻塞!
  • 时刻t4:新来的事务c执行 insert → 被mdl写锁请求阻塞!
  • ...

所有新请求都被阻塞 → 连接池耗尽 → 服务不可用

根本原因:mdl锁是公平的,写锁请求排队后,新的读锁请求也要排队

3.3 如何避免 mdl 锁雪崩

-- 设置 ddl 等待超时(不要无限等待)
set lock_wait_timeout = 5;  -- 等5秒拿不到锁就放弃
alter table orders add column remark varchar(255);

-- 如果报错 lock wait timeout exceeded → 换个时间再试

四、大表 ddl 变更方案

4.1 pt-online-schema-change(percona 工具)

原理

1.创建新表

create table orders_new like orders;

2.新表加字段

alter table orders_new add column remark varchar(255);

3.在原表创建3个触发器:

   - after insert → insert into orders_new
   - after update → update orders_new(或replace into)
   - after delete → delete from orders_new

4. 分批复制原表数据到新表:

   insert into orders_new select * from orders where id between 1 and 1000;
   insert into orders_new select * from orders where id between 1001 and 2000;
   ...

5. 数据追平后,rename table 原子交换:

rename table orders to orders_old, orders_new to orders;

6. 删除触发器,删除旧表

优点

  • 执行期间原表可正常读写
  • 分批复制,不会一次性占用大量资源

缺点/限制

  • 触发器开销:高频写入时,每次 dml 都额外触发一次写操作(写放大 2x)
  • 高频写入可能追不上:复制期间增量太多,永远追不平
  • rename 需要短暂独占锁:如果有长事务持有 mdl 读锁,rename 会等待
  • 不支持有触发器的表:原表已有触发器时冲突

使用示例

pt-online-schema-change \
  --alter "add column remark varchar(255) default null" \
  --host=db.example.com \
  --port=3306 \
  --user=admin \
  --password=secret \
  --chunk-size=1000 \
  --max-load="threads_running=50" \
  --critical-load="threads_running=100" \
  d=mydb,t=orders \
  --execute

关键参数:

  • --chunk-size=1000:每批复制 1000 行
  • --max-load:负载超过阈值时暂停复制
  • --critical-load:负载超过临界值时终止操作

4.2 gh-ost(github 工具)

原理

与 pt-osc 的关键区别:不使用触发器!改为监听 mysql binlog 捕获增量变更。

1. create table orders_ghost like orders;

2. alter table orders_ghost add column remark varchar(255);

3. 启动 binlog 监听线程,实时捕获原表的变更事件

4. 分批复制原表数据到 ghost 表

5. binlog 增量实时追加到 ghost 表

6. 数据追平后,rename 交换(原子操作)

为什么比 pt-osc 更适合高频写入场景

对比pt-osc(触发器)gh-ost(binlog)
增量同步方式触发器(同步,在 dml 事务内)binlog 异步消费
对写入性能的影响大(每次 dml 多一次触发器操作)小(binlog 是 mysql 本身就在写的)
高频写入追赶能力弱(触发器自身是瓶颈)强(binlog 消费是独立线程)
可控性强(可以暂停、限速、测试模式)

使用示例

gh-ost \
  --alter="add column remark varchar(255) default null" \
  --database=mydb \
  --table=orders \
  --host=db.example.com \
  --user=admin \
  --password=secret \
  --chunk-size=1000 \
  --max-load="threads_running=50" \
  --ok-to-drop-table \
  --execute

4.3 instant ddl(mysql 8.0.12+)

-- 瞬间完成,不受数据量影响
alter table orders 
add column remark varchar(255) default null comment '备注',
algorithm=instant;

-- 验证是否支持 instant
alter table orders 
add column remark varchar(255) default null,
algorithm=instant, lock=none;
-- 如果不支持会报错:algorithm=instant is not supported

instant 支持的操作:

  • 加可空列(末尾)
  • 加有默认值的列
  • 修改 enum/set 增加值
  • 修改列默认值
  • 删除列默认值

instant 不支持的操作:

  • 加索引
  • 改列类型/长度
  • 删除列(8.0.29+ 支持)
  • 在非末尾位置加列(8.0.29+ 支持)

4.4 停服变更(最后手段)

# 1. 确认低峰期(凌晨2-4点)
# 2. 通知相关方

# 3. 停止服务
# 或:停止所有实例的 java 进程

# 4. 确认无连接

# 5. 执行 ddl(无并发,瞬间获取mdl锁)
alter table orders add column remark varchar(255) default null;
-- 4000万行约 5-15 分钟

# 6. 确认执行完成
desc orders;  -- 确认新字段存在

# 7. 启动服务

五、方案选择决策流程

ddl 变更需求
  │
  ├─→ 确认 mysql 版本
  │     ├── 8.0.12+ 且操作支持 instant → 直接 instant ddl(秒级)
  │     └── 5.7 或不支持 instant ↓
  │
  ├─→ 评估表数据量
  │     ├── < 100 万 → 直接 alter(online ddl,通常 < 1分钟)
  │     └── > 100 万 ↓
  │
  ├─→ 评估写入频率
  │     ├── 低频(< 100 tps) → pt-osc 或 dms 无锁变更
  │     └── 高频(> 100 tps) ↓
  │
  ├─→ 评估数据量 + 写入频率组合
  │     ├── < 1000 万 + 高频 → gh-ost(binlog 方式)
  │     └── > 1000 万 + 高频 ↓
  │
  ├─→ 是否可以新建子表替代
  │     ├── 可以 → 新建子表(零 风险,推荐)
  │     └── 不可以 ↓
  │
  └─→ 申请停服窗口
        → 低峰期停服 → alter table → 启动服务

六、通用示例代码

6.1 ddl 变更前的评估脚本

-- 1. 查看表数据量
select table_name, table_rows, 
       round(data_length/1024/1024, 2) as data_mb,
       round(index_length/1024/1024, 2) as index_mb
from information_schema.tables 
where table_schema = 'mydb' and table_name = 'orders';

-- 2. 查看当前表的活跃连接(是否有长事务持有mdl锁)
select * from information_schema.processlist 
where db = 'mydb' and command != 'sleep' 
order by time desc;

-- 3. 查看 mysql 版本(决定是否可用 instant)
select version();

-- 4. 查看表结构(确认当前字段)
show create table orders\g

-- 5. 估算 alter 耗时(测试环境执行)
set profiling = 1;
alter table orders_test add column remark varchar(255);
show profiles;

6.2 安全执行 ddl 的包装脚本

-- 设置超时保护(避免长时间等待mdl锁导致雪崩)
set session lock_wait_timeout = 10;  -- 最多等10秒
set session innodb_lock_wait_timeout = 10;

-- 执行 ddl
alter table orders add column remark varchar(255) default null comment '备注';

-- 如果超时报错:error 1205 (hy000): lock wait timeout exceeded
-- → 说明有长事务持有锁,需要找到并处理后重试

6.3 查找阻塞 ddl 的长事务

-- 查找持有 mdl 锁的事务
select 
    t.trx_id,
    t.trx_state,
    t.trx_started,
    timestampdiff(second, t.trx_started, now()) as running_seconds,
    t.trx_mysql_thread_id,
    p.user,
    p.host,
    p.db,
    p.command,
    p.info as current_sql
from information_schema.innodb_trx t
join information_schema.processlist p on t.trx_mysql_thread_id = p.id
order by t.trx_started asc;

-- 如果发现运行很久的事务,可以 kill(需确认不影响业务)
-- kill <trx_mysql_thread_id>;

6.4 新建子表替代方案(java 示例)

/**
 * 方案b:新建轻量子表存储新字段.
 * 适用于:大表无法 alter 时,将新字段存到独立小表.
 */
// 1. 新表 ddl(瞬间完成,空表)
// create table order_extend_new (
//     id int auto_increment primary key,
//     order_id int not null,
//     new_field_a int default null,
//     new_field_b varchar(100) default null,
//     unique key uk_order_id (order_id)
// );
// 2. 实体类
@entity
@table(name = "order_extend_new")
public class orderextendnew extends baseentity {
    @column(name = "order_id")
    private integer orderid;
    @column(name = "new_field_a")
    private integer newfielda;
    @column(name = "new_field_b")
    private string newfieldb;
}
// 3. repository
public interface orderextendnewrepository extends jparepository<orderextendnew, integer> {
    orderextendnew findbyorderid(integer orderid);
}
// 4. 写入(在原有事务内一起写)
@transactional
public void createorder(orderdto dto) {
    order order = orderrepository.save(buildorder(dto));
    // 写新扩展表
    if (dto.getnewfielda() != null) {
        orderextendnew extend = new orderextendnew();
        extend.setorderid(order.getid());
        extend.setnewfielda(dto.getnewfielda());
        orderextendnewrepository.save(extend);
    }
}
// 5. 读取(多一次查询)
public orderdto getorder(integer orderid) {
    order order = orderrepository.findbyid(orderid).get();
    orderdto dto = converttodto(order);
    // 从新表读扩展字段
    orderextendnew extend = orderextendnewrepository.findbyorderid(orderid);
    if (extend != null) {
        dto.setnewfielda(extend.getnewfielda());
    }
    return dto;
}

6.5 预留备用字段的表设计

-- 建表时预留扩展字段,避免日后 alter
create table order_detail (
    id int auto_increment primary key,
    order_id int not null,
    item_sku_id int not null,
    qty int not null,
    price decimal(14,2) not null,
    -- 业务字段...
    
    -- 预留扩展字段(日后需要时直接使用,无需alter)
    ext_int_1 int default null comment '预留整型字段1',
    ext_int_2 int default null comment '预留整型字段2',
    ext_int_3 int default null comment '预留整型字段3',
    ext_str_1 varchar(255) default null comment '预留字符串字段1',
    ext_str_2 varchar(255) default null comment '预留字符串字段2',
    ext_str_3 varchar(255) default null comment '预留字符串字段3',
    
    create_time datetime not null,
    update_time datetime not null,
    index idx_order_id (order_id)
) comment='订单明细表';

-- 使用时只需更新注释(不改结构,无需ddl):
alter table order_detail 
modify column ext_int_1 int default null comment '是否定日达 0否1是';
-- modify column 改注释在 mysql 5.7 中也是 online 的

七、数据归档方案

7.1 为什么要归档

表持续增长:

  2024年1月:1000万行 → alter 2分钟
  2024年6月:2000万行 → alter 5分钟
  2025年1月:3000万行 → alter 10分钟
  2025年6月:4000万行 → alter 15分钟 + dms失败!

归档后:

  • 热表只保留近3个月数据:500万行 → alter 1分钟
  • 历史表按月分:每月1表,每表约500万行

7.2 归档示例

-- 1. 创建归档表
create table orders_archive_202601 like orders;

-- 2. 迁移旧数据到归档表
insert into orders_archive_202601 
select * from orders where create_time < '2026-02-01';

-- 3. 删除热表中的旧数据(分批删除,避免长事务)
delete from orders where create_time < '2026-02-01' limit 10000;
-- 重复执行直到删除完毕

-- 4. 定时任务自动归档(每月1日执行)

八、经验教训总结

#教训规避措施
14000 万 + 高频写入 = 在线 ddl 工具也可能失败提前在测试环境验证,准备备选方案
2表数据量是需要持续治理的,不能放任增长建立数据归档机制,设置表大小告警
3dms 无锁变更有适用边界,不是万能的了解工具原理,评估是否适用当前场景
4新功能字段优先考虑新表养成习惯:大表不加字段,小表加字段
5mysql 版本是基础设施投资推动升级 8.0,instant ddl 彻底解决问题
6停服变更虽然粗暴但最可靠建立低峰期停服变更的标准流程和审批机制
7ddl 变更需要和业务方协同纳入需求评审环节,提前识别大表变更风险

以上就是mysql数据库大表ddl变更操作的原理、方案与实践教学的详细内容,更多关于mysql大表ddl操作的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com