零停机数据库迁移实战:Schema 变更与数据迁移策略
数据库迁移是线上系统最危险的操作之一——一个 ALTER TABLE 可能锁住整个表、拖垮生产、甚至丢数据。本文梳理四类常见迁移场景的零停机方案:加字段、改类型、拆表、异构迁移,每类附策略模板和回滚方案。【点击查看四类迁移模板】
先说结论:零停机迁移的关键是”并轨运行”
数据库迁移的核心矛盾在于:Schema 必须迁移,但线上服务不能停。 解决思路不是”更快地完成迁移”,而是”让新旧结构在一段时间内并存,应用层兼容两者,等旧数据全部迁完再切走”。
本文覆盖四类场景,每类给出策略模板和回滚方案。
场景一:加字段(最简单的零停机操作)
策略:三段式渐进法
-- 第 1 步:加字段,允许 NULL(不锁表,元数据操作)
ALTER TABLE users ADD COLUMN phone varchar(20);
-- 第 2 步:应用层分批次填充(后台 Job,每次 1000 条)
UPDATE users SET phone = '' WHERE phone IS NULL LIMIT 1000;
-- 第 3 步:确认全部填充后,加 NOT NULL 约束
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
为什么这样安全?
- 第 1 步:MySQL 5.6+/PostgreSQL 加允许 NULL 的字段是 O(1) 元数据操作,不锁表
- 第 2 步:分批次避免长事务和主从延迟
- 第 3 步:操作前用
SELECT count(*) FROM users WHERE phone IS NULL确认干净
回滚:直接 DROP COLUMN 即可,不影响数据。
场景二:改列类型(最危险的一类)
策略:新增列 + 数据同步 + 应用双写 + 原子切换
-- 第 1 步:加新列
ALTER TABLE orders ADD COLUMN total_amount_cents bigint;
-- 第 2 步:应用层开启双写(写旧列同时也写新列)
-- 代码: orders.totalAmount = amount; orders.totalAmountCents = Math.round(amount * 100);
-- 第 3 步:后台 Job 回填历史数据(按 id 分批)
UPDATE orders SET total_amount_cents = ROUND(total_amount::numeric * 100)
WHERE total_amount_cents IS NULL AND id BETWEEN ? AND ?;
-- 第 4 步:切换读路径(应用从读旧列切到读新列)
-- 代码: const amount = orders.totalAmountCents / 100;
-- 第 5 步:验证一致后,删除旧列
ALTER TABLE orders DROP COLUMN total_amount;
双写期越长越安全:建议至少运行 48 小时的双写,观察数据一致性,确认无误后再切读路径。
回滚:切读路径之前随时可回滚——停止双写、删新列即可。
场景三:拆表(单表过大时的水平拆分)
表超过一定规模(MySQL 建议单表不超过 5000 万行,PG 可以更大但取决于查询模式),需要拆表来维持性能。
策略:Proxy + 双写 + 全量同步
状态 0: 应用 → 旧表(orders)
状态 1: 应用 → 旧表 + 影子表双写(orders_2026)
状态 2: 后台 Job 将旧表全量同步到影子表(分批)
状态 3: 应用读切到影子表,旧表保留只读备份
状态 4: 确认稳定后,移除旧表
业界成熟的中间件方案:
| 方案 | 适用数据库 | 特点 |
|---|---|---|
| ProxySQL + pt-archiver | MySQL | 规则灵活,支持灰度路由 |
| pg_partman | PostgreSQL | 原生分区管理,自动创建子表 |
| Vitess | MySQL | 完整分布式方案,适合大规模拆分 |
| Citus | PostgreSQL | 分布式扩展,SQL 兼容性好 |
场景四:异构迁移(换数据库类型)
比如从 MySQL 迁到 PostgreSQL,或者从自建 MongoDB 迁到托管版。这类迁移风险最高,因为 SQL 方言、数据类型、事务行为都不同。
策略:双写 + 数据校验 + 灰度流量
状态 0: 应用 → MySQL
状态 1: 应用 → MySQL + PostgreSQL 双写(写两边,读走 MySQL)
状态 2: 历史数据全量 + 增量同步(工具如 pgloader、Debezium)
状态 3: 全量对比校验一致后,灰度切读(先切 1% 流量到 PG)
状态 4: 逐步放大灰度比例(1% → 10% → 50% → 100%)
状态 5: 确认稳定后,下线 MySQL 读依赖
灰度切流是异构迁移最重要的安全措施——先用 1% 流量跑一段时间,观察应用错误率和性能,确认没问题再放大。灰度期间问题只影响 1% 的用户,你可以从容回滚。
通用工具链
| 工具 | 适用场景 | 原理 |
|---|---|---|
| gh-ost | MySQL Online DDL | Binlog 流式同步,无触发器 |
| pt-online-schema-change | MySQL DDL | 触发器 + 影子表 |
| pgroll | PostgreSQL 无锁迁移 | 影子表 + 触发器 |
| pgloader | 异构迁移(任意 DB → PG) | 流式批量加载 |
| Debezium | CDC(变更数据捕获) | 解析 binlog/WAL → Kafka |
| pt-table-checksum | MySQL 数据校验 | 分块 CRC32 比对 |
典型迁移时序(以 gh-ost 为例)
17:00 创建影子表(空)
17:01 启动 binlog listener,开始增量追踪
17:02 开始全量复制(300 万行,预计 2 小时)
19:05 全量完成,进入"追赶延迟"阶段
19:08 延迟降至 0(binlog 追上)
19:08 🔒 短暂申请表锁(几毫秒)
19:08 原子切换:影子表 ↔ 原表改名
19:08 解锁
从开始到完成约 2 小时,其中真正的”不可用窗口”只有 几十毫秒(锁表改名)。
检查清单
迁移前:
- 确认磁盘空间足够(影子表需要额外空间)
- 关闭迁移目标表的定时任务和触发器
- 备份原表(
CREATE TABLE ... AS SELECT或 mysqldump) - 灰度环境先跑一次完整的迁移演练
迁移中:
- 监控主从延迟(增量同步阶段延迟应保持在 5 秒以内)
- 监控磁盘 IOPS 和 CPU(全量复制阶段压力最大)
- 确认回滚方案已就绪(回滚脚本已写在纸上,不是”到时再想”)
迁移后:
- 数据校验:行数 + 校验和 + 抽样全字段
- 运行至少 24 小时再清理旧表
- 更新文档和监控面板
相关阅读
- 监控告警体系设计:从指标采集到告警收敛 — 迁移完成后的监控覆盖
- Redis 缓存策略与常见陷阱 — 数据库迁移后的缓存层加速方案
- Docker Compose 部署实战:从开发到生产的配置演进 — 数据库迁移的容器化部署
需要数据库迁移方案设计的帮助?联系我们 获取免费咨询与报价。
常见问题
加一个非空字段(NOT NULL)一定要停服吗?
不一定。标准的零停机做法是拆成三步:先 ADD COLUMN 允许 NULL(不锁表),然后分批次用应用层逻辑填充默认值,最后再 ALTER COLUMN SET NOT NULL。SQLite 除外——SQLite 的 ALTER TABLE 能力有限,加 NOT NULL 需要重建整表。
Mythril(Online Schema Change)工具可靠吗?
成熟的迁移工具如 gh-ost(GitHub)、pt-online-schema-change(Percona)在生产环境经过数万次验证,原理是通过触发器或 binlog 同步在影子表上执行变更,完成后原子切换。gh-ost 不依赖触发器,用 binlog 流式同步,对主库压力更小,是 MySQL 迁移的首选。
PostgreSQL 做零停机迁移和 MySQL 有什么区别?
PostgreSQL 的 DDL 支持事务(可以在事务块里执行 ALTER TABLE,失败自动回滚),并且某些操作(如加字段)不锁表。但 ALTER TYPE 变更列类型会重写整表,大表仍需工具辅助。pgroll 是 PostgreSQL 生态的零停机迁移工具,支持回滚和细粒度控制。
数据迁移后怎么验证数据一致性?
至少做三层验证:①行数校验——两边表的 COUNT 一致;②校验和校验——对关键字段计算 CRC32 或 MD5,比较两边的值;③抽样全字段比对——随机抽取 10%-20% 的记录,逐字段比对。推荐工具:pt-table-checksum(MySQL)、pgverifiy(PostgreSQL)。