Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL大表DDL操作

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

作者:霸道流氓气质

本文将详细解析MySQL数据库中DDL底层原理,对比pt-osc、gh-ost和INSTANT DDL等在线变更方案,教你如何避免MDL锁雪崩,安全高效地完成大表结构变更,附完整决策流程和实战经验,助你轻松应对生产环境挑战

一、什么是 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 是难题

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

二、MySQL DDL 的底层原理

2.1 MySQL 5.6 之前(Copy 方式)

ALTER TABLE orders ADD COLUMN remark VARCHAR(255);

执行过程:

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

2.2 MySQL 5.6+ Online DDL(Inplace 方式)

ALTER TABLE orders ADD COLUMN remark VARCHAR(255), ALGORITHM=INPLACE;

执行过程:

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

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

2.3 MySQL 8.0.12+ INSTANT DDL

ALTER TABLE orders ADD COLUMN remark VARCHAR(255), ALGORITHM=INSTANT;

执行过程:

限制:

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 不冲突:

3.2 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. 删除触发器,删除旧表

优点

缺点/限制

使用示例

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

关键参数:

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 支持的操作:

INSTANT 不支持的操作:

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失败!

归档后:

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操作的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文