数据库迁移指南
为什么数据库迁移至关重要
数据库迁移(Database Migration)是应用程序生命周期中最关键也最高风险的操作之一。每次修改表结构、添加索引或调整约束,都可能影响数据完整性、查询性能甚至导致服务不可用。没有版本化的迁移管理,团队很容易陷入"在我机器上能跑"的困境,生产环境与开发环境的数据库结构逐渐分裂。本指南涵盖 12 款主流迁移工具对比、零停机策略、PostgreSQL / MySQL 实战语法、完整迁移清单和常见陷阱,帮助你安全、可靠地管理数据库变更。
迁移工具对比(12 款)
| 工具 | 语言 | 方式 | 回滚支持 | 最适合 |
|---|---|---|---|---|
| Flyway | Java / CLI | 版本化 SQL 文件(V1__name.sql) | 付费版 Undo | Java 应用、SQL 优先团队 |
| Liquibase | Java / CLI | XML / YAML / SQL 变更日志 | 自动 rollback | 多数据库、复杂变更集 |
| golang-migrate | Go / CLI | 编号 .up.sql / .down.sql | down 文件手写 | Go 应用、简单 SQL 迁移 |
| Alembic | Python | Python 脚本含 upgrade / downgrade | downgrade 函数 | SQLAlchemy / Python 项目 |
| Prisma Migrate | TypeScript | 基于 Schema Diff 自动生成 | 手动编写 down | Node.js / TypeScript 应用 |
| Atlas | Go / CLI | 声明式(目标状态)或版本化 | 自动生成逆向 | 现代 Go 应用、CI/CD |
| Knex.js | JavaScript | JS/TS 迁移函数(up / down) | down 函数手写 | Node.js / Express 项目 |
| TypeORM | TypeScript | 自动检测实体变更生成迁移 | down 函数自动生成 | TypeScript 全栈应用 |
| Sequelize | JavaScript | JS 迁移文件(up / down) | down 函数手写 | Node.js 传统项目 |
| Django Migrations | Python | 从 Model 自动检测生成迁移 | 自动生成逆向 | Django Web 应用 |
| Rails ActiveRecord | Ruby | DSL 迁移文件(change / up / down) | 自动可逆或 down | Ruby on Rails 项目 |
| dbmate | Go / CLI | 纯 SQL .up.sql / .down.sql | down 文件手写 | 轻量级、语言无关项目 |
迁移文件目录结构
golang-migrate(编号 up/down)
Flyway(V__ / R__ / U__)
Prisma(自动生成)
Django(自动生成)
常用迁移操作(PostgreSQL & MySQL)
创建表
添加列(带默认值 — 零停机安全)
删除列(扩展-收缩)
重命名列(扩展-收缩)
添加索引(不锁表)
修改列类型
添加外键
零停机迁移策略
扩展-收缩模式(Expand-Contract)
这是最通用的零停机方案,分 5 步完成:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1. 扩展 | 添加新列/表 | 只增不删,不影响现有代码 |
| 2. 回填 | 迁移旧数据到新结构 | 可分批处理避免锁定 |
| 3. 双写 | 部署代码同时写入新旧结构 | 保证迁移期间数据一致 |
| 4. 切读 | 部署代码从新结构读取 | 旧结构降级为备用 |
| 5. 收缩 | 删除旧列/表 | 确认无回退需求后执行 |
蓝绿数据库部署
维护两套数据库(Blue / Green)。在 Green 库执行迁移,测试通过后切换流量。适合大版本升级,但需要双倍存储和数据同步机制(如逻辑复制)。
Online DDL 工具
| 工具 | 数据库 | 原理 |
|---|---|---|
| pt-online-schema-change | MySQL | 创建影子表 → 安装触发器 → 拷贝数据 → 原子重命名 |
| gh-ost | MySQL | 基于 binlog 复制变更,不使用触发器,更安全 |
| pg_repack | PostgreSQL | 在线重建表和索引,释放膨胀空间 |
| pgroll | PostgreSQL | 多版本 Schema 共存,渐进式迁移 |
影子表方式(Shadow Table)
迁移执行清单
| 阶段 | 操作 | 说明 |
|---|---|---|
| 迁移前 | 全量备份数据库 | pg_dump / mysqldump / 快照 |
| 迁移前 | 在 staging 环境用生产数据副本测试 | 确保迁移脚本在真实数据上正确运行 |
| 迁移前 | 评估 DDL 变更的锁持有时长 | 对大表使用 pg_stat_activity 检查活跃锁 |
| 迁移前 | 准备回滚脚本并验证可用性 | 在 staging 先跑一遍 down 迁移 |
| 迁移前 | 通知团队迁移时间窗口 | 协调发布冻结期 |
| 迁移中 | 监控活跃连接数和主从延迟 | 延迟 > 阈值则暂停 |
| 迁移中 | 设置 statement_timeout / lock_timeout | 避免无限等锁导致连接堆积 |
| 迁移中 | 管理连接池 | 必要时临时扩大连接池或暂停非关键服务 |
| 迁移后 | 验证行数和数据完整性 | 对比迁移前后的 COUNT / CHECKSUM |
| 迁移后 | 监控慢查询和索引使用情况 | 检查 pg_stat_user_indexes 或 EXPLAIN |
| 迁移后 | 性能基准测试 | 对比核心查询响应时间 |
| 收尾 | 确认旧代码完全下线后再删废弃列 | 保留至少 1-2 个发布周期 |
常见陷阱
- 直接在生产环境跑未测试的迁移 — 永远先在 staging 用生产数据副本验证。一条有语法错误的 SQL 可能留下半完成的迁移状态。
- 没有回滚脚本 — 每个 up 迁移都应有对应的 down。发现问题时没有回滚脚本,只能手动修复,风险极高。
- 大表长时间锁定 — ALTER TABLE 在大表上可能持锁数分钟甚至数小时。使用 CONCURRENTLY、pt-online-schema-change 或 gh-ost 避免。
- 不可逆的数据丢失 — DROP COLUMN 不可恢复。确保删列前数据已迁移到新列,并保留备份。
- 添加 NOT NULL 列不带默认值 — 会导致旧行无法满足约束。先 ADD COLUMN 带 DEFAULT,再根据需要修改。
- 忘记更新 ORM 模型 — 迁移修改了数据库结构,但代码中的实体/模型未同步更新,导致运行时错误。
- 迁移顺序冲突 — 多人并行开发时,迁移编号冲突或依赖顺序错误。使用时间戳命名而非顺序编号可缓解。
工具快速入门
golang-migrate
Flyway
Prisma Migrate
Alembic
常见问题 (FAQ)
A: 首先检查数据库当前状态(是否部分执行)。如果使用支持事务 DDL 的数据库(如 PostgreSQL),失败的迁移会自动回滚。MySQL 的 DDL 不支持事务,需要手动修复。大多数工具有 dirty 状态标记,修复后需要手动设置版本号(如 migrate force N)。
A: 取决于团队和场景。自动生成(Prisma、Django、Alembic autogenerate)适合快速迭代,但可能遗漏数据迁移逻辑。手写 SQL 迁移对性能敏感操作(索引、大表变更)更精确可控。推荐:自动生成 + 人工审查后再提交。
A: 数据迁移应与 Schema 迁移分开管理。先做 Schema 变更(添加新列),再用独立脚本或迁移步骤回填数据。大数据量回填建议分批处理(每批 1000-10000 行),避免长事务和锁争用。
A: 在部署管道中,迁移应在应用启动前执行。推荐流程:CI 阶段用临时数据库验证迁移(up → down → up),CD 阶段先执行 migrate up,成功后再滚动更新应用容器。使用 Kubernetes 时可配置 init container 执行迁移。
A: 声明式(Atlas、Prisma)定义期望的最终状态,工具自动计算 diff — 适合新项目和快速原型。版本化(Flyway、golang-migrate)显式控制每一步变更 — 适合生产环境要求审计追踪和精确控制的场景。许多团队在开发阶段用声明式,生产部署时导出为版本化迁移。