2721 字
约 9 分钟
21
S06-X04 · Flyway:数据库版本迁移的落地方式

结构变更真正危险的地方不是 DDL 写错——那跑一次就报错。危险的是没人知道某个字段是谁、在什么时候、因为什么加的。等到三套环境(本地、测试、生产)的表结构出现微妙差异,加一个 NOT NULL 列就能让上线卡住一整晚。

Flyway 把"改表"这件事变成一组按版本号排序、带校验和、有执行记录的 SQL 文件。它不做 ORM,也不做 DDL 生成,只保证一件事:同一个版本号,在任何环境上跑出同一个结果。


1. 没有迁移工具时会发生什么

做法 短期 长期
手工在客户端执行 DDL 快 无记录,环境间漂移
代码里 CREATE TABLE IF NOT EXISTS 自动 只能加不能改,改列类型无处安放
JPA ddl-auto=update 省事 不删列不改类型,且不可审查
一份 schema.sql 全量重建 干净 生产不能重建,历史数据没法走
迁移脚本 + 版本表 有成本 可审计、可重放、可回滚到某个版本点

2. 命名规则:两个下划线是硬约定

形式 含义 执行时机
V1__init.sql 版本化迁移,版本 1 未执行过就按序执行
V1_1__add_tag.sql 版本 1.1,下划线即小数点 同上,排在 1 之后
V20261109_1200__add_index.sql 时间戳版本 同上,团队协作首选
R__refresh_view.sql 可重复迁移,无版本号 checksum 变化时重跑
U1__undo_init.sql 回滚脚本 仅 Teams 版支持,社区版无效

V 和描述之间必须是两个下划线 __。写成 V1_add_tag.sql 会被忽略——不是报错,是静默跳过。这是新手第一个坑,因为 Flyway 启动日志里根本不会提这个文件。

3. flyway_schema_history 表

Flyway 第一次运行会在目标 schema 建这张表,之后每次迁移都往里插一行:

列 含义 排查时的用途
installed_rank 执行顺序号(自增) 判断实际执行次序,而非版本号次序
version 脚本版本 与 V 前缀对应
description 描述 人读
type SQL / JDBC / BASELINE 认出基线那行
script 文件名 定位文件
checksum 文件内容的 CRC32 validate 失败时的证据
installed_by 执行者 追责与审计
installed_on 执行时间 与环境发布时间对照
execution_time 毫秒数 长迁移的耗时记录
success 是否成功 false 的行会阻塞后续迁移
-- 查看当前版本与是否有失败记录
SELECT installed_rank, version, description, success, installed_on
FROM flyway_schema_history
ORDER BY installed_rank DESC
LIMIT 10;

success=0 的行必须处理掉,否则后面每个 V 脚本都会因为"存在失败的迁移"而拒绝执行。

4. baseline:接管已有库

baseline 是为"表已经存在、但没有历史记录"的库准备的。第一次接入时有两种写法:

场景 配置 结果
空库,从头建 什么都不配 从 V1 开始全跑
已有库,结构等于 V1 baseline-on-migrate=true 写入一行 BASELINE(version=1),V1 被跳过,V2 起执行
已有库,结构领先于 V1 baseline-version=5 基线定在 5,V1–V5 全部跳过
想显式控制时机 先跑 flyway baseline 再迁移 多一步,但不会误触发

关键理解:baseline 不创建任何表结构,它只在历史表里写一条"到这里为止算已完成"。所以 baseline-version 必须等于"现有结构实际对应的那个版本",写大了会漏掉本该执行的脚本,写小了会在已有表上重跑建表语句直接报错。

5. validate 与 checksum 冲突

validate-on-migrate 默认 true。每次启动前 Flyway 对比磁盘脚本的 CRC32 与历史表里的记录,不一致就拒绝启动:

Validate failed: Migration checksum mismatch for migration version 1.2
-> Applied to database : -1234567890
-> Resolved locally    : 1234567890

处理分两类,别混:

情况 正确做法 错误做法
脚本被格式化工具改了空格/注释 还原内容;或 flyway repair 更新 checksum 直接改历史表
真的需要改结构 写新的 V 脚本 改已执行的 V1.2
迁移中途失败留下 success=0 flyway repair 清掉失败行再重跑 手删一行了事(可能漏清理)

repair 只修"记录",不重跑 SQL。它的语义是"让历史表重新对齐磁盘现状",不是"再执行一次"。

6. locations:多目录与方言隔离

spring:
  flyway:
    enabled: true
    locations: classpath:db/migration,classpath:db/migration/mysql
    encoding: UTF-8
    table: flyway_schema_history
    baseline-on-migrate: true
    baseline-version: 0
    validate-on-migrate: true
    out-of-order: false
    fail-on-missing-locations: true
    placeholders:
      app_schema: clarity
键 默认 说明
locations classpath:db/migration 多目录逗号分隔,同一版本号只能出现一次
baseline-version 1 与已有结构对齐
out-of-order false 开 true 才允许补跑低版本
fail-on-missing-locations true 配错路径直接启动失败,这是好事
placeholders.* 无 脚本里用 ${app_schema} 引用
table flyway_schema_history 多服务共用库时必须改名

多目录的常见用法是按方言拆:db/migration 放通用 DDL,db/migration/mysql 放 MySQL 专有语法。但不要把同一版本号在两个目录里各放一份,Flyway 会报 Found more than one migration with version。

7. 与 JPA 的冲突:必须关掉 ddl-auto

spring:
  jpa:
    hibernate:
      ddl-auto: validate   # 生产建议 validate 或 none
    properties:
      hibernate:
        hbm2ddl:
          auto: none
ddl-auto 与 Flyway 共存的结果
update 两边都改表,谁先谁后不确定,update 会悄悄加列
create / create-drop 启动时删表重建,迁移记录瞬间失真
validate 只校验,不修改,冲突会以启动失败暴露
none 完全不碰,完全交给 Flyway

validate 的价值在于:它会在启动时比对实体映射与真实表结构,字段类型不匹配立刻报错——这正好是迁移脚本漏写时的第一道闸。

8. 团队协作:版本号冲突的预防

两个人同一天各写一个 V12__xxx.sql,后合并的人会遇到两种结果:如果对方已经执行过 V12,自己的脚本 checksum 不同 → validate 失败;如果还没执行,两个 V12 同版本 → Flyway 直接拒绝。

方案 做法 代价
时间戳版本号 V202611091200__xxx.sql 号长,但基本不撞
日期区间预分配 每人当天固定号段 需要约定纪律
CI 前置校验 合并前跑 flyway info,检查有无重复版本 多一条流水线步骤
单点归口 迁移脚本由一个人合并 有瓶颈

CI 里最实用的一条是解析 flyway_schema_history 与本地脚本做差集,发现"待执行版本号小于当前最高版本"就失败——这能在合并前抓住 out-of-order 场景。

9. 不可回滚的迁移:forward-only

MySQL 的 DDL 是隐式提交的,一个脚本里三条 DDL 挂在第二条失败,前一条已经生效且无法回退。所以正确姿势不是写 U 脚本,而是:

  1. 一个脚本只做一件事,把大变更拆成多个 V;
  2. 加列、回填、加约束分三次提交,中间版本允许空值;
  3. 出问题不改旧脚本,写新的 V 修正;
  4. 删列这类破坏性操作放到"确认无引用"的后续版本,先改名下线。
-- V10__add_article_status.sql   只加列,允许 NULL
ALTER TABLE article ADD COLUMN status TINYINT NULL;

-- V11__backfill_article_status.sql  回填,仍不加约束
UPDATE article SET status = 1 WHERE status IS NULL;

-- V12__tighten_article_status.sql   最后收紧
ALTER TABLE article MODIFY COLUMN status TINYINT NOT NULL DEFAULT 1;

这样每个版本的失败面都很小,回滚只需要部署上一版应用,数据库保持向前。

10. clean 为什么必须禁用

clean 会 drop 掉目标 schema 里的所有对象,包括历史表和业务数据。Flyway 9 之后默认 cleanDisabled=true,Spring Boot 也把 spring.flyway.clean-disabled 默认设为 true。

要警惕的是有人为了"本地重来一次"把它设成 false 并提交上去,而 spring.flyway.url 又是从环境变量读的——一旦某次环境变量指向了预发库,mvn flyway:clean 就是一次全库删除。

防线 做法
配置层 只在 application-dev.yaml 里关掉 clean-disabled
账号层 生产迁移账号不给 DROP 权限
流程层 禁止在 CI 里出现 flyway:clean

11. 排错

现象 原因 处理
启动报 Found non-empty schema without schema history table 已有库未配 baseline 加 baseline-on-migrate=true 并定好版本
报 Detected resolved migration not applied to database 版本低于当前最高且 out-of-order=false 改版本号,或临时开 out-of-order
报 Migration checksum mismatch 已执行脚本被改 还原;确需改则 repair
脚本完全没被执行 文件名缺 __ 或不在 locations 检查命名与路径
Unable to obtain connection 但应用能连库 spring.flyway.url 与 spring.datasource.url 各写一份且不一致 只留 datasource,Flyway 会复用
迁移卡住很久 大表加索引/改列,MySQL 锁表 拆脚本,或用 ALGORITHM=INPLACE 评估

12. 边界

场景 更合适的做法 理由
只做一次性的数据修复 运维脚本 + 审批 不该进版本历史
多租户按 schema 隔离 每租户一个 Flyway 实例 单实例只认一个 schema
需要严格回滚 迁移 + 应用双版本滚动 + 兼容期 数据库回滚本身不可靠
结构由代码生成(如多租户动态表) 运行期建表 + 独立元数据表 Flyway 是静态脚本
分库分表后的物理表 初始化脚本 + 逻辑表统一迁移 物理表数量会爆炸

13. 自测

  1. V1_add_tag.sql 为什么会被静默跳过?
  2. baseline-on-migrate=true 之后,V1 会不会执行?为什么?
  3. repair 修好 checksum 之后,那条迁移会重新跑一遍吗?
  4. 生产为什么建议 ddl-auto=validate 而不是 none?
  5. MySQL 下为什么"写一个 U 脚本回滚"不成立?
  6. clean-disabled 之外,还有哪两道防线能防住误清库?
S06-X04 · Flyway:数据库版本迁移的落地方式
https://www.clxhxhhr.top/posts/5651/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。