数据库迁移与版本化:Flyway / Liquibase
代码有 Git,配置有 Git,可数据库的表结构呢?很多团队的表结构活在 DBA 的电脑里、活在某个人手动敲的 SQL 里、活在"上个月那次紧急上线我改过但忘了记"里。结果是:新同事拉下代码跑不起来、测试环境少一个字段、生产环境多一个索引,而且谁都不敢删那个没人认识的列。这一章讲怎么把表结构也纳入版本控制——让"改数据库"变成一次可评审、可回滚、可重放、可审计的代码提交。两个主流工具:Flyway(简单直接)和 Liquibase(强大抽象)。
11.1 先看清问题:手工改库的四种典型事故
| 事故 | 怎么发生的 | 版本化之后怎么解决 |
|---|---|---|
| 环境漂移 | 开发在本地加了列,测试环境手动加了,生产忘了加。上线报 Unknown column | 迁移脚本随代码走,各环境自动执行同一份脚本,结构与代码版本一一对应 |
| 新人跑不起来 | 新人克隆代码,跑建表脚本,缺了最近半年的十几个 ALTER,本地一堆报错 | 从空库按顺序重放全部脚本,一次就得到与生产一致的结构 |
| 无法回滚 | 改错了只能靠记忆写反向 SQL,边写边冒汗 | Liquibase 可自动生成回滚;Flyway 用"向前修复(forward-only)"策略——回滚靠新脚本而不是撤旧脚本 |
| 无法审计 | 三个月后没人记得那个索引是谁加的、为什么加,删也不敢删 | 每次结构变更都是一次带提交信息、评审记录、执行时间记录的变更 |
论核心思想:把"改结构"当"改代码"
迁移工具的模型其实非常朴素:
① 一组有序的迁移脚本(脚本就是资源文件,跟着代码进 Git)。
② 数据库里一张"已执行记录"表(Flyway 叫 flyway_schema_history,Liquibase 叫 databasechangelog)。
③ 启动时比对:把"脚本清单"和"已执行清单"对一下,只执行没跑过的那些,并且按顺序执行。
就这么三件事。理解了它,你就明白两条铁律:已经执行过的脚本不能再改内容(否则"已执行的校验和"对不上,工具会直接报错拒绝启动);表结构只能向前走,不能靠删历史脚本解决。
11.2 Flyway:约定优于配置的 SQL 迁移
Flyway 是最好上手的迁移工具,因为它几乎不要求你学新东西——迁移脚本就是普通的 SQL 文件,只是命名要守规矩。(当前稳定版本 Flyway 11.x,以官网为准;社区版免费,企业版才有undo撤销等特性。)
迁移脚本怎么写(注意:写的时候就要考虑"生产上的大表")
Spring Boot 集成:加依赖 + 配置,启动时自动迁移
论flyway_schema_history 这张表你必须认识
Flyway 执行过的每个脚本都会在这张表里留一行:版本号、描述、类型、脚本名、校验和(checksum)、执行人、执行耗时、成功与否。
它带来两个极有用的能力:① 启动时先比对校验和——如果有人把已经执行过的脚本改了内容,Flyway 会直接抛 Migration checksum mismatch 并拒绝启动应用(这是特意的"刺耳失败",比默默带着错误结构跑要好得多);② 它是"当前数据库处于哪个版本"的唯一权威答案,排障时先看它。
还有一条必须知道的:MySQL 8.0 的 DDL 是原子的(一条 ALTER 要么成功要么回滚),但一个脚本里的多条 DDL 不是原子的。如果 V3 的第 2 条 SQL 执行失败了,脚本会记成"失败"状态,你修完脚本后必须先 repair 清掉那条失败记录,才能再执行。所以一个迁移脚本里最好只做一件事。
11.3 Liquibase:用 changeset 描述"我要什么"
Flyway 是"我直接写 SQL",Liquibase 是"我说我要什么变更,它翻译成各数据库的 SQL"。这个抽象让同一套 changelog 能跑在 MySQL、PG、Oracle 上(跨库迁移场景很有价值),并天然支持回滚、前置条件、上下文(按环境决定执行哪些变更)。(当前稳定版本 Liquibase 4.x,以官网为准。)
论三个核心概念:changelog / changeset / changeType
changeset:最小执行单元,用 id + author 唯一标识(它俩+文件名一起决定这条变更的身份)。一个 changeset 要么整体执行成功,要么整体失败。
changelog:一个有序的 changeset 列表(可以是主文件 include 多个子文件),也就是"迁移清单"。
changeType:具体动作,如 createTable、addColumn、createIndex。它由 Liquibase 翻译成数据库方言的 SQL——这就是跨库能力的来源,也是"不够灵活"的来源(要写复杂 SQL 时还得用 sql 类型嵌原生 SQL)。
用 YAML 写 changelog(也可以选 XML / JSON / SQL)
需要写复杂 SQL 时:用 sql 类型嵌原生语句(但要注意这是"不可移植"的)
11.4 Flyway 还是 Liquibase:一张表说清
| 维度 | Flyway | Liquibase |
|---|---|---|
| 迁移脚本形态 | 纯 SQL(也有 Java 迁移) | XML / YAML / JSON / SQL,抽象成 changeType |
| 学习成本 | 极低——会写 SQL 就会用 | 要学 changeset / preConditions / contexts 等概念 |
| 跨数据库 | 要自己写各库方言(或分目录) | 强项:同一份 changelog 可跑多库(用抽象 changeType 时) |
| 回滚 | 社区版不支持 undo,走"向前修复":写一个新脚本改回去 | 支持 rollback,可自动生成反向语句或显式定义 |
| 复杂 SQL 表达力 | 强(就是原生 SQL,窗口函数、存储过程随便写) | 弱一些,复杂逻辑要退回 sql 类型,此时跨库优势也没了 |
| 执行记录表 | flyway_schema_history(含校验和) | databasechangelog + databasechangeloglock |
| 适合谁 | 单一数据库(比如就用 MySQL)的团队、想快速上手、SQL 水平好 | 多数据库支持需求、合规/审计要求回滚能力、团队更愿意接受"描述式"配置 |
选怎么快速定
只有一个数据库、团队 SQL 熟练 → Flyway。它就是"把 ALTER 语句收进 Git 并按顺序自动执行",几乎零心智负担,是目前 Java 生态的主流选择。
要同时支持多种数据库、或者必须能回滚 → Liquibase。尤其是产品需要交付给客户、由客户在自己机房的 Oracle / SQL Server 上部署时,抽象层的价值就体现出来了。
两个都不选的前提是:你的库很小、只有一个人维护、并且你愿意接受"某天发现测试库和生产库结构不一样"的惊喜。一般不建议。
11.5 生产实践:迁移脚本的五条纪律与四个坑
纪五条必须守的纪律
① 已执行的脚本永不修改。校验和不匹配会让应用拒绝启动。要改结构就新增一个更高版本的脚本。这是最重要的一条。
② 一个脚本只做一件事。方便失败时判断"执行到哪了",也方便单独回滚或临时跳过。
③ 结构变更和数据变更分开。把 ALTER TABLE 和几百万行的 UPDATE 写在同一个脚本里,一旦数据更新耗时太长或超时,整个脚本会被标记失败,结构也卡在半路。
④ 变更必须"向前兼容"(expand-contract)。因为发布过程中新旧版本的代码会同时运行(滚动发布),所以改字段要分三步走,不能一步到位。
⑤ 迁移脚本进 CI 门禁。每次 PR 都在一个干净的空库上完整跑一遍全部脚本(flyway:validate + 迁移 + 冒烟查询)。这样能在合并前就发现"新人跑不起来"的问题。
expand-contract:改字段名的安全三步法(举例:把 username 改名成 user_name)
① 大表加索引把库锁了。MySQL 8.0 支持 Online DDL(加索引一般 ALGORITHM=INPLACE 不锁表),但有些操作(如改列类型、加 NOT NULL 又无默认值、修改字符集)会退化成拷贝整表,几千万行的表能锁几分钟到几十分钟。上线前必须确认执行方式(看 ALTER 时的算法),必要时用 gh-ost / pt-online-schema-change 在线改。
② 有人手工改过生产库。迁移一跑就报错(列已存在 / 校验和不对)。不要删表重建,也不要改历史脚本。正确做法:写一个幂等的修复脚本(ADD COLUMN IF NOT EXISTS 之类,MySQL 不支持的话就先查 information_schema 再决定),或者用 flyway:repair / liquibase clearCheckSums 修正元数据(这段操作要在评审后进行)。
③ 生产开过 clean / drop-first。配置里 clean-disabled: true、drop-first: false 是保命配置,一定要在生产 profile 里显式打开。历史上不止一个团队在改配置时误把生产库清空。
④ 多实例同时启动跑迁移。Kubernetes 里一次起 10 个 Pod,10 个进程同时尝试执行迁移,容易互相打架(好在工具都会用锁表——Flyway 靠元数据表的锁、Liquibase 靠 databasechangeloglock,但依然建议把迁移拆成独立的 Job 或 initContainer 单独执行一次,别让业务进程兼任)。
① 表结构也要版本化。手工改库换来的短期方便,代价是环境漂移、新人跑不起来、无法回滚、无法审计。
② 迁移工具的模型只有三件事:有序脚本 + 已执行记录表 + 启动时比对补齐。
③ Flyway 是"把 SQL 收进 Git",简单直接,单库场景首选;Liquibase 是"描述变更 + 自动翻译 + 支持回滚",跨库和合规场景更合适。
④ 铁律:历史脚本永不修改,改结构只能加新版本脚本。
⑤ 大表变更走 expand-contract(加新列 → 双写回填 → 切读删旧列),并用 gh-ost 之类工具做在线 DDL,别在生产上直接来一条阻塞式 ALTER。