← 返回博客

零停机数据库迁移实战: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-archiverMySQL规则灵活,支持灰度路由
pg_partmanPostgreSQL原生分区管理,自动创建子表
VitessMySQL完整分布式方案,适合大规模拆分
CitusPostgreSQL分布式扩展,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-ostMySQL Online DDLBinlog 流式同步,无触发器
pt-online-schema-changeMySQL DDL触发器 + 影子表
pgrollPostgreSQL 无锁迁移影子表 + 触发器
pgloader异构迁移(任意 DB → PG)流式批量加载
DebeziumCDC(变更数据捕获)解析 binlog/WAL → Kafka
pt-table-checksumMySQL 数据校验分块 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 小时再清理旧表
  • 更新文档和监控面板

相关阅读

需要数据库迁移方案设计的帮助?联系我们 获取免费咨询与报价。

常见问题

加一个非空字段(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)。

本文来自 AI Enable Harness 一线交付实践。需要同类系统或优化服务?

订阅博客更新

新文章发布后第一时间邮件通知。不定期发送,不推销。

订阅 →