数据库迁移实战:Flyway 与 Liquibase 版本化管理

数据库迁移(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 怎么选

维度FlywayLiquibase
迁移表达方式以 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 TABLETRUNCATE 这类破坏性语句要格外警惕,最好通过评审红线;第三,数据类变更能否回滚——”把某状态字段从 0/1 改成枚举”这种涉及既有数据的更新,必须配套回滚方案或双写过渡。把迁移脚本纳入和代码同级的 Pull Request 评审,能把绝大多数线上结构事故挡在合并之前。

八、与存量库共存

老项目往往已经手工建好了一批表。直接引入迁移工具会因”库里没历史记录”而把建表语句再跑一遍、报已存在错误。解决方法是先做一次基线对齐:Flyway 设 baseline-on-migrate: true 并把 baseline-version 调到当前版本;Liquibase 则把已存在表登记进 DATABASECHANGELOG。对齐之后,后续所有变更都走脚本,存量结构继续由工具托管。

结语

数据库迁移工具的真正价值,不是”帮你执行一条 SQL”,而是把schema 演进变成可版本化、可回滚、可审计的工程资产。Flyway 胜在极简、贴合 SQL 直觉,适合单一数据库、团队熟悉 SQL 的场景;Liquibase 胜在声明式、跨库与强回滚,适合多数据库与强合规需求。无论选哪个,核心都只有一句话:让每一次表结构变更都进仓库、走流水线、留痕迹。当你把数据库迁移纳入 CI/CD,那些”生产环境到底改没改”的凌晨惊魂,就从此成为历史。

上一篇 大模型提示注入攻防:AI 应用安全实战
下一篇 远程协作异步沟通实战:工程师高效协作指南