Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

后端工程 / Backend Engineering

1. Backend 后端技术全景学习路线 / A Complete Backend Engineering Learning Path

2. 分布式系统——为什么单体时代过去了 / Distributed Systems Beyond the Monolith

3. 并发编程——为什么你的库存总是扣成负数 / Concurrency Control and Inventory Consistency

4. MySQL 优化——为什么你的查询总是那么慢 / MySQL Query and Storage Optimization

5. 架构模式——什么时候该用 CQRS / Architecture Patterns and When to Use CQRS

6. 系统设计——如何设计一个每秒 10 万订单的秒杀系统 / Designing a High-Throughput Flash-Sale System

7. 数据库工程化——在线表结构变更怎么不停服 / Database Engineering and Online Schema Changes

8. 可观测性——出了问题怎么快速定位 / Observability and Rapid Production Diagnosis

本页目录

数据库工程化——在线表结构变更怎么不停服 / Database Engineering and Online Schema Changes ​

📅 创建时间:2026-05-08 🏷️ 标签:#数据库迁移 #在线DDL #主从延迟 #数据备份 #Flyway 📚 前置知识:[[00-backend-overview]] [[09-mysql-optimization]](MySQL 基础) 📚 相关知识:[[/03-web/06-databases-and-data-access/01-mysql]](MySQL 进阶)


场景:产品经理说,给 orders 表加一个字段 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  产品经理:给 orders 表加一个 coupon_code 字段。          │
│  DBA:好的。ALTER TABLE orders ADD COLUMN coupon_code VARCHAR(50); │
│                                                             │
│  执行时间:ALTER TABLE ...                                  │
│  线上表 5000 万行,ALTER 要 30 分钟                      │
│                                                             │
│  影响:                                                  │
│  ❌ ALTER 期间表锁,整个 orders 表不可写                │
│  ❌ 主从延迟:从库延迟 30 分钟                        │
│  ❌ 业务中断:下单接口超时,用户无法下单                │
│                                                             │
│  DBA:...能不停服吗?                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这一章,我们理解数据库工程化的核心问题:在线变更、主从延迟、备份恢复。


第1节:在线表结构变更——pt-osc 和 gh-ost ​

问题:ALTER TABLE 为什么会锁表 ​

┌─────────────────────────────────────────────────────────────┐
│                 MySQL ALTER 的过程                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  MySQL 5.6 之前:                                       │
│  ALTER TABLE orders ADD COLUMN coupon_code VARCHAR(50);    │
│  → 重建整张表                                            │
│  → 全局写锁(MDL X Lock)                               │
│  → 30 分钟内无法写入                                    │
│                                                             │
│  MySQL 5.6+ Online DDL:                                │
│  → 允许并发 DML(部分场景)                            │
│  → 但仍可能需要全表锁                                  │
│  → 无法保证不锁表                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

pt-online-schema-change(Percona Toolkit) ​

┌─────────────────────────────────────────────────────────────┐
│              pt-osc 原理                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Step 1:创建新表(空表)                               │
│  orders_new (包含新字段 coupon_code)                     │
│                                                             │
│  Step 2:创建触发器(同步增量数据)                     │
│  → INSERT 触发器:INSERT INTO orders_new VALUES (NEW.*)  │
│  → UPDATE 触发器:UPDATE orders_new SET ... = NEW....    │
│  → DELETE 触发器:DELETE FROM orders_new WHERE ...       │
│                                                             │
│  Step 3:拷贝历史数据                                    │
│  SELECT * FROM orders ORDER BY id LIMIT 1000             │
│  → INSERT INTO orders_new VALUES (...)                   │
│  (分批拷贝,不锁表)                                     │
│                                                             │
│  Step 4:切换表名                                        │
│  RENAME TABLE orders TO orders_old, orders_new TO orders  │
│  (元数据锁,极短)                                       │
│                                                             │
│  Step 5:删除旧表和触发器                                │
│  DROP TABLE orders_old                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bash
# pt-osc 命令
pt-online-schema-change \
    --alter "ADD COLUMN coupon_code VARCHAR(50) DEFAULT NULL" \
    --user=root \
    --password=xxx \
    --charset=utf8mb4 \
    --execute \
    D=mydb,t=orders

# 关键参数
--alter-foreign-keys-method=rebuild_constraints  # 外键处理
--max-load="Threads_running=50"                  # 超过 50 并发时暂停
--critical-load="Threads_running=100"             # 超过 100 并发时退出
--chunk-size=1000                                # 每批处理行数
--max-lag=5                                      # 允许的最大延迟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

gh-ost(GitHub Online Schema Change) ​

┌─────────────────────────────────────────────────────────────┐
│              gh-ost vs pt-osc 的区别                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  pt-osc:                                               │
│  → 使用触发器同步增量数据                               │
│  → 触发器有性能开销                                    │
│  → 对外键处理复杂                                      │
│                                                             │
│  gh-ost(推荐):                                      │
│  → 不使用触发器,用 Binlog 同步增量                   │
│  → 性能更好                                            │
│  → 支持暂停/恢复                                      │
│  → 支持动态调整速率                                    │
│                                                             │
│  gh-ost 原理:                                         │
│  1. 分析表结构,创建新表                              │
│  2. 从旧表拷贝数据(binlog 记录偏移量)              │
│  3. 监听 Binlog,用 Binlog 同步增量                  │
│  4. 切换表名                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bash
# gh-ost 命令
gh-ost \
    --database="mydb" \
    --table="orders" \
    --alter="ADD COLUMN coupon_code VARCHAR(50)" \
    --execute \
    --user="root" \
    --password="xxx" \
    --host="mysql-master" \
    --allow-on-master \
    --chunk-size=1000 \
    --max-load=Threads_running=50 \
    --critical-load=Threads_running=100 \
    --postpone-cut-over-flag-file=/tmp/gh-ost-pause

# 支持动态暂停
touch /tmp/gh-ost-pause   # 暂停
rm /tmp/gh-ost-pause      # 恢复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第2节:数据库迁移管理——Flyway ​

问题:多个环境的数据库版本不一致 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  开发环境:orders 表有 20 个字段                        │
│  测试环境:orders 表有 18 个字段                        │
│  生产环境:orders 表有 19 个字段                        │
│                                                             │
│  为什么?                                              │
│  → 开发手动改了测试环境的表结构                         │
│  → 没有记录改了什么                                    │
│  → 现在不知道生产环境和测试环境的差异在哪里          │
│                                                             │
│  解决:数据库迁移工具(Flyway / Liquibase)           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Flyway 工作原理 ​

┌─────────────────────────────────────────────────────────────┐
│                 Flyway 版本管理                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  SQL 迁移文件命名规范:                                  │
│  V1__create_orders_table.sql                              │
│  V2__add_user_index.sql                                  │
│  V3__add_coupon_code_column.sql                          │
│  V4__create_order_items_table.sql                        │
│                                                             │
│  Flyway 工作流程:                                        │
│  1. 检查 flyway_schema_history 表                         │
│  2. 找出未执行的迁移(V1-V4 中缺失的)                 │
│  3. 按顺序执行迁移                                      │
│  4. 记录到 flyway_schema_history                         │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  flyway_schema_history:                           │ │
│  │  version | description          | executed_at     │ │
│  │  -------+---------------------------+-----------  │ │
│  │  1      | create_orders_table     | 2026-05-01   │ │
│  │  2      | add_user_index          | 2026-05-02   │ │
│  │  3      | add_coupon_code_column  | 2026-05-03   │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

Flyway 迁移文件示例 ​

sql
-- V1__create_orders_table.sql
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
1
2
3
4
5
6
7
8
9
10
11
12
sql
-- V2__add_coupon_code_column.sql
-- 注意:使用 gh-ost 兼容语法
ALTER TABLE orders
    ADD COLUMN coupon_code VARCHAR(50) DEFAULT NULL
    COMMENT '使用的优惠券编码'
    AFTER total_amount;
1
2
3
4
5
6
sql
-- V3__create_order_cancellation_log.sql
-- 数据迁移:如果有取消订单的记录,需要迁移
CREATE TABLE order_cancellation_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL,
    reason VARCHAR(500),
    cancelled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 数据迁移(如果需要)
INSERT INTO order_cancellation_log (order_id, reason, cancelled_at)
SELECT id, cancel_reason, updated_at
FROM orders
WHERE status = 'cancelled' AND cancel_reason IS NOT NULL;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python
# Flyway Python API
from flyway import Flyway

flyway = Flyway()
flyway.setDataSource(
    "jdbc:mysql://localhost:3306/mydb",
    "username",
    "password"
)

# 迁移
flyway.migrate()

# 只检查当前版本(不执行迁移)
flyway.info()

# 清理(危险!)
flyway.clean()  # 删除所有表,慎用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第3节:主从延迟——从库数据比主库慢 ​

问题:读写分离后,数据不一致 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  架构:主库写入,从库读取                                │
│                                                             │
│  用户下单:                                              │
│  POST /order → 写入主库 → 返回"下单成功"            │
│                                                             │
│  用户立刻查询:                                          │
│  GET /order → 读取从库 → 订单不存在!                  │
│                                                             │
│  原因:从库比主库慢了 5 秒                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

主从延迟的原因 ​

┌─────────────────────────────────────────────────────────────┐
│              MySQL 主从复制原理                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  主库:                                              │
│  事务提交 → 写入 Binlog → 返回客户端                 │
│                                                             │
│  从库 IO Thread:                                        │
│  读取主库 Binlog → 写入 Relay Log                     │
│                                                             │
│  从库 SQL Thread:                                       │
│  读取 Relay Log → 执行 SQL → 更新从库                 │
│                                                             │
│  延迟来源:                                             │
│  1. SQL Thread 是单线程执行(MySQL 5.6 之前)      │
│  2. 大事务:一个事务写入 100 万行                    │
│  3. 从库负载高:查询多,CPU/IO 繁忙                │
│  4. 网络延迟:Binlog 传输慢                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

解决主从延迟 ​

┌─────────────────────────────────────────────────────────────┐
│              解决主从延迟的方案                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  方案 1:并行复制(MySQL 5.7+)                       │
│  → 从库 SQL Thread → 多个 Worker Thread               │
│  → 同一事务的 SQL 并行执行                           │
│  → 大幅减少延迟                                        │
│  → 配置:slave_parallel_workers=8                       │
│                                                             │
│  方案 2:读从库失败时读主库                           │
│  def read_from_replica(user_id):                        │
│      try:                                                │
│          return replica.query(f"SELECT * FROM users WHERE id={user_id}")  │
│      except ReplicationLagException:                      │
│          return master.query(f"SELECT * FROM users WHERE id={user_id}")  │
│                                                             │
│  方案 3:强制读主库(关键场景)                       │
│  → 下单后立即展示订单 → 强制读主库                   │
│  → 其他场景读从库                                      │
│                                                             │
│  方案 4:等主从同步完成后再返回                       │
│  → select wait_for_master_to_sync()                     │
│  → 延迟可接受(通常 <1 秒)                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

第4节:数据备份与恢复 ​

三种备份策略 ​

┌─────────────────────────────────────────────────────────────┐
│              数据库备份策略                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 全量备份                                            │
│  → 备份整个数据库                                       │
│  → 恢复简单,但备份时间长(GB/TB 级)                │
│  → 频率:每周一次                                       │
│                                                             │
│  2. 增量备份(Binlog 备份)                           │
│  → 备份自上次备份以来的 Binlog                        │
│  → 备份快,但恢复慢(需要逐个应用 Binlog)           │
│  → 频率:每小时一次                                     │
│                                                             │
│  3. 增量备份(LSN 备份)                              │
│  → 备份特定 LSN 之后的变更                           │
│  → XtraBackup 使用此方式                               │
│                                                             │
│  推荐组合:                                             │
│  每周全量 + 每日增量 + 实时 Binlog                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

XtraBackup 备份实战 ​

bash
# 全量备份
xtrabackup --backup \
    --target-dir=/backup/full \
    --user=root \
    --password=xxx \
    --compress \
    --parallel=4

# 增量备份(基于上次备份)
xtrabackup --backup \
    --target-dir=/backup/incr_1 \
    --incremental-basedir=/backup/full \
    --user=root \
    --password=xxx

# 恢复
# 1. 解压
xtrabackup --decompress --target-dir=/backup/full

# 2. 应用日志
xtrabackup --prepare --target-dir=/backup/full
xtrabackup --prepare --target-dir=/backup/incr_1 --incremental-basedir=/backup/full

# 3. 恢复数据
xtrabackup --copy-back --target-dir=/backup/full
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

第5节:数据库变更流程规范 ​

┌─────────────────────────────────────────────────────────────┐
│              数据库变更 SOP                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Step 1:评审                                            │
│  → DDL 是否会影响业务                                   │
│  → 预计耗时                                             │
│  → 是否需要停服窗口                                      │
│                                                             │
│  Step 2:测试环境验证                                    │
│  → 在测试环境执行,观察影响                             │
│  → 记录执行时间                                         │
│                                                             │
│  Step 3:预发布环境验证                                  │
│  → 使用 gh-ost / pt-osc 执行                            │
│  → 监控主从延迟                                        │
│                                                             │
│  Step 4:生产环境执行                                    │
│  → 低峰期执行(通常凌晨)                               │
│  → DBA 监控                                            │
│  → 准备好回滚方案                                       │
│                                                             │
│  Step 5:验证                                            │
│  → 检查表结构                                           │
│  → 检查数据完整性                                       │
│  → 确认应用日志无异常                                   │
│                                                             │
│  一句话:                                                │
│  变更前备份,变更后验证,出问题立即回滚。               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

"AI 可查 vs 必须理解"清单 ​

AI 可查:
✅ pt-osc / gh-ost 的具体命令参数
✅ Flyway 的配置参数
✅ XtraBackup 的备份命令

必须理解:
🔴 pt-osc 和 gh-ost 的原理区别
🔴 为什么主从复制会有延迟
🔴 并行复制如何减少延迟
🔴 全量备份 + 增量备份的组合策略
1
2
3
4
5
6
7
8
9
10

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇6. 系统设计——如何设计一个每秒 10 万订单的秒杀系统 / Designing a High-Throughput Flash-Sale System
下一篇8. 可观测性——出了问题怎么快速定位 / Observability and Rapid Production Diagnosis

持续记录,持续成长

Copyright © Tidenflow