数据库迁移(Database Migration)是每一个长期维护的系统都绕不开的工程环节。业务在变,表结构必然要跟着变:加字段、建索引、拆表、改类型……如果还停留在”本地用 Navicat 连上库、手动执行一条 ALTER“的阶段,迟早会出事——测试环境改了、生产环境忘了;多人协作互相覆盖;回滚无据可查。MySQL 深度调优 再到位,也救不了一次”漏改字段导致线上 500″的发布事故。本文把 schema 变更变成”像代码一样可版本化、可回滚、可审计”的流程,对比 Flyway 与 Liquibase 两种主流方案,并给出可直接落地的脚本与 CI/CD 衔接。
一、为什么不能用”手工改表”
手工执行 DDL 的本质问题,是把”环境状态”和”代码版本”割裂开了。Git 能告诉你应用代码在什么版本,却无法告诉你数据库处在什么版本。于是每次发布都伴随着”我到底有没有在预发把那张表加上”的焦虑。更致命的是:一旦多人同时改库,或者蓝绿发布期间新旧代码并存,缺少统一迁移历史就会酿成结构不一致。成熟的团队会把 schema 当作代码来管理——每一次变更都是一个”带版本号的迁移脚本”,由工具自动按序执行并记录。
二、Flyway:约定优于配置的 SQL 派
Flyway 的设计哲学极其简单:迁移脚本文件名即版本。它扫描指定目录,按文件名里的版本号顺序执行,并把每一条执行记录写进一张 flyway_schema_history 表(含版本、校验和、执行时间)。核心概念只有三个:版本号(如 V1、V2)、描述(双下划线后)、校验和(防篡改)。
2.1 写一段 SQL 迁移
-- V1__create_users.sql
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX uk_users_username ON users(username);
-- V2__add_email.sql
ALTER TABLE users ADD COLUMN email VARCHAR(128);
文件名规则是 V{版本}__{描述}.sql,双下划线分隔。Flyway 会按版本号升序执行,已执行过的版本绝不再跑。
2.2 Spring Boot 集成
在 Spring Boot 项目中,只要把脚本放到 resources/db/migration,加上配置即可在应用启动时自动迁移:
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
baseline-on-migrate 对”已存在旧库”的场景特别有用:它会把当前版本标记为基线,避免对已手工建好的表重复执行。命令行同样简单:flyway -url=jdbc:mysql://localhost:3306/demo -user=root -password=xxx migrate。
三、Liquibase:声明式、跨数据库的变更集
Liquibase 走的是另一条路线:它用一份声明式的 changelog 描述”要达成什么状态”,再由 Liquibase 翻译成不同数据库的 DDL。changelog 支持 XML、YAML、JSON、SQL 四种格式,最常用的是 YAML,可读性好。
3.1 写一份 YAML changelog
databaseChangeLog:
- changeSet:
id: 1-init
author: zhang
changes:
- createTable:
tableName: orders
columns:
- column: { name: id, type: bigint, autoIncrement: true, constraints: { primaryKey: true } }
- column: { name: user_id, type: bigint, constraints: { nullable: false } }
- column: { name: amount, type: decimal(10,2), constraints: { nullable: false } }
- changeSet:
id: 2-add-index
author: zhang
changes:
- createIndex:
tableName: orders
indexName: idx_orders_user
columns: [user_id]
每个 changeSet 有唯一 id + author 作为身份标识,执行后写入 DATABASECHANGELOG 表。Liquibase 还能用 preConditions 做前置校验、用 rollback 标签定义回滚逻辑,这是它相对 Flyway 的一大优势。
3.2 Spring Boot 集成
spring:
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.yaml
接入后应用启动即自动执行未应用的 changeSet。若你已经习惯了 Swagger/SpringDoc 这类 Spring Boot 生态工具,Liquibase 的集成体验会非常顺滑。
四、Flyway vs Liquibase 怎么选
| 维度 | Flyway | Liquibase |
|---|---|---|
| 迁移表达方式 | 以 SQL 脚本为主(也支持 Java) | 声明式 changelog(XML/YAML/JSON/SQL) |
| 跨数据库能力 | 弱,SQL 与具体库强绑定 | 强,一份 changelog 适配多种库 |
| 回滚支持 | 需自行写 undo 脚本(V 版不支持自动回滚) | 可声明 rollback 逻辑,支持自动回滚 |
| 学习曲线 | 极低,会写 SQL 即可 | 稍陡,需理解 changeSet 模型 |
| 适用场景 | 单一数据库、团队熟悉 SQL | 多数据库、需强回滚与审计 |
五、接入 CI/CD:让迁移随发布自动执行
迁移脚本既然是代码,就应当进仓库、随流水线执行。最稳妥的姿势是:应用启动前由独立的迁移步骤先把库升到目标版本,再发布新代码,避免代码先跑、表还没建导致的启动失败。下面是一段 GitHub Actions 里的迁移步骤:
- name: Migrate Database
run: |
flyway -url="${{ secrets.DB_URL }}" \
-user="${{ secrets.DB_USER }}" \
-password="${{ secrets.DB_PASSWORD }}" \
migrate
把数据库账号以 secret 注入,迁移在部署 job 中早于应用部署执行。这样每次合并到主干,结构变更都自动、可追溯地落地。
六、回滚策略与常见坑
| 问题 | 正确做法 |
|---|---|
| 误加字段要回退 | 提前写好 undo/rollback 脚本,或通过新迁移反向 ALTER,勿直接手工改 |
| 生产库已有数据但无迁移历史 | 用 baseline(Flyway)或 changelog 基线标记,跳过已存在的表 |
| 团队协作版本冲突 | 版本号用时间戳或分支前缀,禁止两人复用同一 V 号 |
| 大表 ALTER 锁表 | Online DDL(MySQL 8 原生)或 pt-online-schema-change,错峰执行 |
| 校验和不匹配(checksum mismatch) | 已执行的脚本绝不修改;确需变更用新版本,保留历史 |
最重要的一条纪律:已执行过的迁移脚本永远只读、不修改。一旦你把 V2 改了,Flyway/Liquibase 计算出的校验和就和表里记录的不一致,会直接报错拒绝执行。要改就发 V3。
七、迁移脚本也要 Code Review
很多团队给应用代码做了严格评审,却把迁移脚本当”一次性操作”直接合入,这是隐患。迁移脚本一旦执行就作用在真实数据上,评审时至少要盯三件事:第一,是否锁表——在大表上加字段、加索引,务必确认走 Online DDL 或第三方工具,避免在业务高峰把整张表锁死;第二,是否幂等安全——脚本里写 DROP TABLE、TRUNCATE 这类破坏性语句要格外警惕,最好通过评审红线;第三,数据类变更能否回滚——”把某状态字段从 0/1 改成枚举”这种涉及既有数据的更新,必须配套回滚方案或双写过渡。把迁移脚本纳入和代码同级的 Pull Request 评审,能把绝大多数线上结构事故挡在合并之前。
八、与存量库共存
老项目往往已经手工建好了一批表。直接引入迁移工具会因”库里没历史记录”而把建表语句再跑一遍、报已存在错误。解决方法是先做一次基线对齐:Flyway 设 baseline-on-migrate: true 并把 baseline-version 调到当前版本;Liquibase 则把已存在表登记进 DATABASECHANGELOG。对齐之后,后续所有变更都走脚本,存量结构继续由工具托管。
结语
数据库迁移工具的真正价值,不是”帮你执行一条 SQL”,而是把schema 演进变成可版本化、可回滚、可审计的工程资产。Flyway 胜在极简、贴合 SQL 直觉,适合单一数据库、团队熟悉 SQL 的场景;Liquibase 胜在声明式、跨库与强回滚,适合多数据库与强合规需求。无论选哪个,核心都只有一句话:让每一次表结构变更都进仓库、走流水线、留痕迹。当你把数据库迁移纳入 CI/CD,那些”生产环境到底改没改”的凌晨惊魂,就从此成为历史。




