Flyway 实战:用版本化管理搞定数据库迁移与回滚

当团队规模扩大、多个环境并行时,手工执行 SQL 改表往往带来「开发库改了、测试库忘了、生产库炸了」的灾难。Flyway 用一套版本化的迁移脚本,把数据库迁移纳入代码仓库与 CI/CD,让每一次 schema 变更都可追溯、可重复、可回滚。

一、为什么需要数据库版本管理

在没有版本化管理之前,数据库结构(schema)往往游离于代码之外:DBA 在图形化工具里手动 ALTER,开发在本地随手加字段,上线前再补一份 SQL 文件。这种模式在单人项目里勉强能跑,一旦遇到多环境、多人协作就会出现三类典型问题。

  • 环境漂移:测试库比生产库多几张表,问题直到上线才暴露。
  • 不可复现:无法从零重建出与生产一致的结构,新同事搭环境全靠口口相传。
  • 无审计:谁在什么时候改了什么,完全说不清,出事只能靠回忆。

Flyway 的核心思想很简单:把每一次 schema 变更写成带版本号的迁移脚本,由工具按版本顺序自动执行。它像代码的版本控制一样,给数据库做「版本控制」。如果你已经在用 GitHub Actions 搭建 CI/CD 流水线,那么把迁移也交给流水线就是顺理成章的一步。

二、Flyway 核心概念

在动手之前,先理清四个关键概念,它们决定了 Flyway 的行为边界。

  • Migration(迁移):一个独立的 schema 变更单元,分版本化迁移(Versioned)与可重复迁移(Repeatable)。
  • Version(版本号):决定执行顺序,如 1、1.1、2。Flyway 会记录已执行的版本,只跑「未执行过的」。
  • Checksum(校验和):脚本内容的哈希,防止已执行的脚本被偷偷改动导致不一致。
  • Baseline(基线):对已有老库打基线,告诉 Flyway「这之前的版本我认了,从某个版本之后才开始管」。

2.1 迁移脚本命名规范

版本化脚本命名必须遵循 V{版本}__{描述}.sql,双下划线分隔版本与描述;可重复脚本以 R__描述.sql 开头。例如下面这组脚本,Flyway 会按 1 → 1.1 → 2 的顺序执行:

V1__create_user_table.sql
V1.1__add_user_email.sql
V2__create_order_table.sql
R__refresh_user_view.sql

2.2 Flyway 如何决定执行哪些脚本

Flyway 会在目标库里维护一张 flyway_schema_history 表,记录每次迁移的版本、文件名、校验和、执行时间。启动时它扫描脚本目录、比对历史表,只执行版本号大于已记录最大版本、且未被执行的脚本,从而保证幂等——重复跑不会重复建表。

三、Spring Boot 集成 Flyway

Spring Boot 对 Flyway 提供开箱即用的自动装配,几乎零配置即可启用。首先引入依赖(以 Maven 为例,PostgreSQL 需额外驱动模块):

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-database-postgresql</artifactId>
</dependency>

然后在 application.yml 中配置数据源与 Flyway 行为:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/demo
    username: demo
    password: demo
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    out-of-order: false

把脚本放到 src/main/resources/db/migration 后,应用一启动 Flyway 就会自动执行。对 PostgreSQL 这类关系型库做 分区表优化 或 慢查询调优 时,结构化的迁移管理正是 Flyway 的用武之地。

四、编写第一个迁移脚本

创建 V1__create_user_table.sql,用纯 SQL 描述结构变更:

CREATE TABLE "user" (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(120),
    created_at TIMESTAMP NOT NULL DEFAULT now()
);

CREATE INDEX idx_user_email ON "user" (email);

注意:PostgreSQL 默认把未加引号的标识符折叠为小写,表名用双引号包裹才能保留大写;这与 PostgreSQL 和 SQL Server 的语法差异 中强调的命名习惯一脉相承。执行后,flyway_schema_history 会多出一条 version=1 的记录,后续再部署这台机器就会跳过它。

五、Java 迁移与回调

除了 SQL,Flyway 还支持用 Java 编写迁移,适合需要编程逻辑的场景(如数据脱敏、批量转换、调用存储过程)。实现 JavaMigration 接口即可:

public class V3__seed_default_roles implements JavaMigration {
    public void migrate(Context context) throws Exception {
        try (Statement stmt = context.getConnection().createStatement()) {
            stmt.execute("INSERT INTO role(name) VALUES ('ADMIN'),('USER')");
        }
    }
    public MigrationVersion getVersion() { return MigrationVersion.fromVersion("3"); }
    public String getDescription() { return "seed default roles"; }
}

回调(Callback)则能在迁移前/后插入钩子,比如迁移完成后自动刷新物化视图,或用 Docker Compose 拉起测试库时预置种子数据,让集成测试环境始终与生产结构对齐。

六、在 CI/CD 中自动迁移

把迁移交给流水线,而不是靠人工登录执行,是工程化的关键一步。下面是一段 GitHub Actions 片段,在部署前先跑迁移,失败则阻断发布:

name: deploy
on: [push]
jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { java-version: '21', distribution: 'temurin' }
      - run: mvn -q flyway:migrate
        env:
          FLYWAY_URL: ${{ secrets.DB_URL }}
          FLYWAY_USER: ${{ secrets.DB_USER }}
          FLYWAY_PASSWORD: ${{ secrets.DB_PWD }}

把数据库密码放进 Secrets、用最小权限账号执行迁移,既安全又可审计。配合预发环境先跑一遍,能挡掉绝大部分「本地能过、线上翻车」的结构问题。

七、生产环境避坑清单

迁移上生产有不少「看起来没事、一上就炸」的坑,整理成一张对照表,建议贴在团队 Wiki 上。

陷阱现象对策
改已执行的脚本checksum 不匹配,启动失败永远新增脚本,不回头改旧版本
大表加列锁表线上 ALTER 卡死、连接打满PG 11+ 多数 DDL 安全;MySQL 走 在线 DDL / pt-osc
多环境顺序错乱out-of-order 误开导致跳版本保持线性版本,关掉 out-of-order
基线设置错误重复执行老表创建报错老库先 flyway baseline 再接管

八、Flyway vs Liquibase 选型

另一个常见选择是 Liquibase,两者定位相似但风格不同,选型时可以从这几个维度对比:

维度FlywayLiquibase
迁移描述原生 SQL 为主,直观XML / YAML / JSON / SQL 多格式
跨库差异贴近具体库方言支持跨库抽象(changelog)
回滚需手写 undo 脚本支持部分自动回滚
上手成本低,约定优于配置略高,配置更灵活

如果你的团队以 SQL 为主、追求简单可控,Flyway 是更轻的选择;若需要跨多种数据库、声明式管理,Liquibase 更合适。二者并不互斥,很多团队会按服务复杂度分别使用。

九、回滚策略与最佳实践

Flyway 社区版默认不自动回滚版本化迁移——这是有意为之,因为结构变更(如删列、改类型)的「反操作」往往不可逆或代价高昂。推荐三种回滚思路:

  1. 补偿式迁移:不删旧脚本,新增 Vn__undo_xxx.sql 把变更抵消掉,保证前向可重放、历史可审计。
  2. 整库还原:依赖备份与 时序/快照机制,发布前先打快照,失败即回滚到快照。
  3. 蓝绿部署:迁移保持向后兼容(先加字段、双写、再删旧字段),与新版本代码并行运行。

最后给几条铁律:迁移脚本只增不改、保持线性版本、把执行权交给 CI、生产迁移先在预发验证。把这些纪律固化进流程,数据库就不再是上线最大的不确定性来源。

结语

Flyway 把一个容易被忽视的环节——数据库结构演进——变成了和代码一样可版本化、可审查、可自动化的资产。配合 CI/CD 与规范的命名约定,它能显著降低「结构漂移」带来的线上事故。下次要改表时,不妨先写一条 V{n}__描述.sql,让 Flyway 替你把风险管起来。

上一篇 WebGPU 实战:用浏览器 GPU 计算与渲染加速前端应用
下一篇 Spring AI 实战:Java 项目如何接入大模型
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]