Flyway:把数据库变更从启动脚本变成可审计的发布协议
一次应用回滚把数据库留在了新版本。旧实例重新启动后仍能连接,却在读取订单时持续报“列不存在”;值班人员发现生产库已经有新列,于是把仓库里的 V014__add_delivery_window.sql 改成了兼容旧代码的写法。下一次发布又被 checksum mismatch 拦住。为了尽快恢复,另一位同事执行 repair,校验转绿了,但旧实例的查询仍然失败,部分数据也没有回到变更前。
这不是 Flyway “记错了状态”,而是团队把三件不同的事混在了一起:
应用版本能否与当前 schema 共存,是兼容性发布问题。migration 文件是否和已执行记录一致,是变更历史完整性问题。数据库对象与业务数据怎样恢复,是数据库恢复问题。
Flyway 负责发现 migration、排序、执行,并把版本、类型、脚本名、checksum、执行结果等状态写入 schema history table。它能阻止一部分历史漂移,却不会自动理解“旧应用需要哪几列”,也不会把一次危险 DDL 变成无锁操作,更不会因为 repair 成功就修复已经被改坏的数据。团队真正要建立的是一份可审计的发布协议:脚本进入版本库,隔离库重放,CI 校验,单一发布者执行,应用按兼容窗口切换,失败时根据数据库事务能力恢复。
先把 migration、数据库状态和发布动作分开
Flyway 最核心的输入是 migration。默认 SQL 命名中,V 表示 versioned migration,R 表示 repeatable migration,较长历史还可以增加 B 类 baseline migration。三者不是同一种脚本换了前缀:
V001__create_customer.sql 有唯一版本,只按版本顺序成功执行一次。表、列、索引、约束和需要留痕的数据修复通常放在这里。R__customer_summary_view.sql 没有版本;checksum 变化后会再次执行,并且总是在本轮待执行的 versioned migration 之后运行。同一轮中的多个 repeatable 按 description 排序。
B100__current_schema.sql 是给新环境加速建库的累计基线。新环境选择可用的最新 baseline migration,再执行其后的版本迁移;已经有 Flyway 历史的环境会忽略它。
Versioned migration 的版本按数值排序,点号和下划线都可以表达层级。团队应固定一种格式,例如连续整数 V001、V002,或者由中心服务分配的单调版本;不要让每个人在长期分支上自行猜下一个编号。两条分支都创建 V023 时,冲突必须在合并前通过重命名解决。已经进入永久下游环境的版本文件不再改名、不再改内容,而是增加新的前向修复版本。
repeatable 适合 CREATE OR REPLACE VIEW、函数、过程和可幂等重建的参考数据,不适合一次性数据搬迁。它的文件必须能重复运行:
-- R__customer_summary_view.sql
CREATE OR REPLACE VIEW customer_summary AS
SELECT c.id,
c.display_name,
COUNT(o.id) AS order_count
FROM customer c
LEFT JOIN customer_order o ON o.customer_id = c.id
GROUP BY c.id, c.display_name;Flyway 会把 repeatable 的文件名和 checksum 记入历史表;checksum 改变时,旧记录变为 Outdated,新定义在下一次 migrate 中执行。不要用每次都变化的占位符强迫它无条件重跑,这会制造无意义 DDL、锁竞争和不可预测的发布时间。若一个脚本必须每轮执行,先确认它其实是不是回调、监测任务或应用初始化,而不是 schema migration。
把实验数据库锁进可随时销毁的边界
CLI 适合开发机诊断和流水线执行;Java API、Maven、Gradle 与 Spring Boot 适合项目集成;容器适合固定运行时并让 CI 复现。安装时从 Flyway command-line 页面选择与操作系统和 CPU 架构匹配的发行包,把解压目录加入 PATH,然后记录版本:
flyway -v
flyway help下载包自带一部分数据库模块和 JDBC driver,但不是所有数据库驱动都随发行包提供。遇到 No Flyway database plugin found 时先检查数据库模块;遇到 No JDBC driver found 再检查 drivers/ 或应用依赖。不要把两类报错都归结为“连接串错误”。升级前应核对目标数据库的 driver reference、支持矩阵和 release notes,并在构建系统中锁定 CLI、插件、数据库模块和 driver 的版本。
下面用两个容器建立一个可销毁的 PostgreSQL 实验。镜像版本由团队基线注入,命令不会悄悄追随 latest:
export DB_IMAGE="postgres:${APPROVED_DB_VERSION:?set APPROVED_DB_VERSION}"
export FLYWAY_IMAGE="redgate/flyway:${APPROVED_FLYWAY_VERSION:?set APPROVED_FLYWAY_VERSION}"
export LAB_PASSWORD="$(openssl rand -hex 18)"
docker network create flyway-lab
docker run -d --rm \
--name flyway-db \
--network flyway-lab \
-e POSTGRES_DB=catalog \
-e POSTGRES_USER=flyway_lab \
-e POSTGRES_PASSWORD="$LAB_PASSWORD" \
--health-cmd='pg_isready -U flyway_lab -d catalog' \
--health-interval=2s \
--health-timeout=2s \
--health-retries=30 \
"$DB_IMAGE"
until [ "$(docker inspect -f '{{.State.Health.Status}}' flyway-db)" = healthy ]; do
sleep 1
done实验目录只需要两个版本文件:
db/
└── migration/
├── V001__create_customer.sql
└── V002__add_customer_status.sql-- V001__create_customer.sql
CREATE TABLE customer (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
display_name VARCHAR(120) NOT NULL
);-- V002__add_customer_status.sql
ALTER TABLE customer ADD COLUMN status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE';先看计划,再执行,再看状态:
docker run --rm --network flyway-lab \
-v "$PWD/db/migration:/flyway/sql:ro" \
-e FLYWAY_URL='jdbc:postgresql://flyway-db:5432/catalog' \
-e FLYWAY_USER='flyway_lab' \
-e FLYWAY_PASSWORD="$LAB_PASSWORD" \
-e FLYWAY_CLEAN_DISABLED='true' \
-e FLYWAY_VALIDATE_MIGRATION_NAMING='true' \
"$FLYWAY_IMAGE" info
docker run --rm --network flyway-lab \
-v "$PWD/db/migration:/flyway/sql:ro" \
-e FLYWAY_URL='jdbc:postgresql://flyway-db:5432/catalog' \
-e FLYWAY_USER='flyway_lab' \
-e FLYWAY_PASSWORD="$LAB_PASSWORD" \
-e FLYWAY_CLEAN_DISABLED='true' \
-e FLYWAY_VALIDATE_MIGRATION_NAMING='true' \
"$FLYWAY_IMAGE" migrate
docker run --rm --network flyway-lab \
-v "$PWD/db/migration:/flyway/sql:ro" \
-e FLYWAY_URL='jdbc:postgresql://flyway-db:5432/catalog' \
-e FLYWAY_USER='flyway_lab' \
-e FLYWAY_PASSWORD="$LAB_PASSWORD" \
"$FLYWAY_IMAGE" info第一次 info 应看到两个 Pending,migrate 应显示成功迁移到版本 002,第二次 info 应看到两个 Success。再从数据库侧核对真实对象,而不是只相信 Flyway 日志:
docker exec -e PGPASSWORD="$LAB_PASSWORD" flyway-db \
psql -U flyway_lab -d catalog -c \
"SELECT column_name, is_nullable, column_default FROM information_schema.columns WHERE table_name='customer' ORDER BY ordinal_position;"
docker exec -e PGPASSWORD="$LAB_PASSWORD" flyway-db \
psql -U flyway_lab -d catalog -c \
'SELECT installed_rank, version, description, type, success FROM flyway_schema_history ORDER BY installed_rank;'列定义和 history 都正确,才证明“迁移执行器的记录”与“数据库的实际状态”在这一刻一致。实验结束后先删数据库容器,再删网络;环境变量随当前 shell 退出失效:
docker stop flyway-db
docker network rm flyway-lab
unset LAB_PASSWORD DB_IMAGE FLYWAY_IMAGESchema History 是状态机,不是可以随手改的日志表
Flyway 默认创建 flyway_schema_history。它记录安装顺序、版本、描述、类型、脚本、checksum、执行者、耗时和成功状态;info 把这些记录与当前 locations 中解析到的 migration 合并,得到 Pending、Success、Missing、Future、Out of Order、Outdated 等状态。
validate 会比较已执行记录与当前可解析 migration 的名称、类型、版本和 checksum。SQL migration 的校验基于 checksum;它回答的是“历史输入是否仍一致”,不回答表结构是否被 DBA 手工改过,也不回答 SQL 是否会锁住大表。把 validate 放进 PR 和发布前流水线,是防止历史脚本被误改或误删的底线:
flyway validate
flyway info -outputType=json > flyway-info.jsonJSON 结果适合由 CI 归档和解析,但连接串、用户名、SQL 文本与异常日志仍要按敏感数据处理。产物保留期应与发布审计要求一致,不能无限保存包含对象名和环境信息的调试日志。
用一个反向实验可以看清 checksum 的价值。先在隔离库执行 V002,再给已经执行过的文件追加一条注释并运行:
flyway validate预期得到 checksum mismatch,退出码非零。正确动作是恢复 V002 的原始内容,再新增 V003__adjust_customer_status.sql。Flyway 读取 SQL migration 时会按配置编码解码,并在 checksum 计算中忽略行结束符差异,因此仅把 LF 转成 CRLF 不应成为校验失败的解释;注释、SQL 文本、编码解释结果以及 repeatable migration 中参与替换的 placeholder 值才需要重点比对。团队仍应固定 UTF-8、格式化边界和 placeholder 来源,使发布制品可逐字节追溯,而不是把所有差异都归因于开发机换行风格。
repair 的名字很容易让人产生错误期待。repair 命令只修 schema history:移除 failed 记录、把已执行记录的 checksum/description/type 对齐到当前文件,并把找不到的 migration 标为 deleted。事务型数据库若已经把失败 migration 整体回滚,history 中通常不会留下可供删除的 failed 行;非事务 DDL 或无法完整回滚的执行路径才更常见“失败记录与半成品对象并存”。repair 不会撤销已经成功的 DDL,不会删除这些半成品对象,也不会恢复业务数据。而且它必须使用与 migrate 相同的 locations,否则可能把未加载到的合法脚本错误标记为 deleted。
因此 checksum mismatch 的处置顺序是:先确认哪个环境执行过、仓库文件为什么改变、原始内容能否从发布制品恢复;只有经过变更审批、确认数据库实际状态与新文件语义完全一致时,才允许执行 repair。把 repair 作为流水线自动重试步骤,会把篡改证据直接擦平。
配置优先级决定你究竟迁移了哪个库
Flyway 支持 TOML、传统 conf、环境变量和命令行参数。当前配置优先级是命令行高于环境变量,环境变量高于标准输入,标准输入高于配置文件;TOML 和传统 conf 还存在模式选择规则。排查“配置明明改了却没生效”时,用 -X 查看加载路径,但在共享日志中先确认调试输出不会暴露 URL 参数或凭据。
一份可提交的 flyway.toml 只放非敏感基线:
[flyway]
locations = ["filesystem:db/migration"]
validateMigrationNaming = true
validateOnMigrate = true
cleanDisabled = true
baselineOnMigrate = false
outOfOrder = false
group = false
lockRetryCount = 30
[environments.default]
connectRetries = 8现代 TOML 把连接属性放在 [environments.<name>],因此 connectRetries 属于 environments.default,而 locations、cleanDisabled 和 lockRetryCount 属于 [flyway]。传统 conf 中的 flyway.connectRetries 仍可兼容,但不能据此把它照搬到 TOML 的 [flyway]。连接地址、用户名和密码由本机 secret store、CI secret、Flyway resolver 或工作负载身份注入。不要把密码放在命令行参数里;进程列表、shell history 和 CI 展开日志都可能记录参数。也不要把完整 JDBC URL 当作无敏感数据,URL 可能包含用户名、证书路径、租户或内部域名。
这些字段改变的是不同故障边界:
| 配置 | 改变的行为 | 团队判断 |
|---|---|---|
locations | 决定哪些 migration 可被解析 | migrate、validate、repair 必须一致 |
schemas / defaultSchema | 决定管理 schema 与 history table 位置 | 多 schema 项目先定义 ownership,避免清错范围 |
table | 修改 history table 名 | 多套迁移链共库时显式隔离,禁止随意改名 |
validateMigrationNaming | 非法命名是否直接失败 | CI 与发布环境保持开启 |
validateOnMigrate | 执行前是否验证历史一致性 | 关闭会把漂移推迟到事故后发现 |
cleanDisabled | 是否拒绝破坏性 clean | 共享、测试、预发、生产都保持 true |
baselineOnMigrate | 非空且无 history 的 schema 是否自动 baseline | 容易把误连数据库伪装成已接管,默认关闭 |
outOfOrder | 是否允许补执行低于当前版本的 migration | 可能导致重放顺序与生产不同,不作为分支冲突修复 |
group / mixed | 是否把一批迁移放进同一事务及是否容许混合 | 受数据库 DDL 事务能力约束,不能跨数据库照搬 |
lockRetryCount | 竞争迁移锁时的重试次数 | 有限等待并告警,不用无限重试掩盖双发布者 |
connectRetries | 环境连接失败后的重试次数 | TOML 放在目标环境下;只吸收短暂启动抖动,不替代网络诊断 |
target | 只迁移到指定版本 | 临时验证可用,长期固定会造成环境停在旧版本 |
配置值应该由环境模板生成并在运行前打印脱敏摘要,例如数据库类型、host 哈希、数据库名、locations、target 和 clean 状态。发布器还要先执行只读身份检查,确认实例标识、数据库名和 schema 白名单;仅凭环境变量名为 PROD 不能证明连接目标正确。
baseline 有两个概念,混用会跳过真实变更
把一个已有数据库纳入 Flyway 时,baseline 命令会创建 history table,并写一条指定 baselineVersion 的基线记录。随后低于或等于该版本的 migration 不再执行。这个动作只声明“目标库已经具备这个版本对应的状态”,不会把实际 schema 与脚本逐对象比对。
安全接管已有库需要先冻结人工 DDL,导出 schema,比较各下游环境,确定一致的起点,再在隔离克隆上重放后续 migration。执行前至少打印目标身份和当前对象摘要:
flyway info
flyway -baselineVersion=100 -baselineDescription='legacy schema accepted' baseline
flyway info若不同下游库并不一致,不能给它们写同一个 baseline 版本后假装统一。应先通过审查过的协调脚本收敛差异,或者为不同状态设计明确的升级路径。baselineOnMigrate=true 会在 migrate 时自动做接管判断,虽然方便,却会削弱“连错库时必须失败”的保护;共享与生产发布器保持关闭,只允许审批过的显式 baseline。
B100__current_schema.sql 则是 baseline migration。它把从空库走到版本 100 的累计结构压成一个新环境入口,不会给已有数据库写 baseline 记录,也不会替代 V001 到 V100 的审计历史。引入 B100 后要同时验证两条路径:
全新数据库选择 B100,再执行 V101 以后版本。已有数据库忽略 B100,继续从自己的历史版本向前迁移。
两条路径最终 schema 应等价。基线脚本若漏掉权限、扩展、序列、触发器或默认值,新环境会和长期升级环境分叉;因此每次更新 baseline migration,都要做结构 diff 和应用冒烟,而不是只看 migrate 成功。
Spring 启动迁移很方便,也会把发布与扩容耦合
Spring Boot 检测到 Flyway 后会在应用上下文启动阶段调用 Flyway.migrate()。当前 Spring Boot 对常见数据库还要求加入对应 Flyway database module;例如 PostgreSQL 项目由 Boot dependency management 锁定兼容版本:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>默认 migration 放在 src/main/resources/db/migration。应用配置只引用外部注入的连接信息:
spring:
jpa:
hibernate:
ddl-auto: validate
flyway:
enabled: true
locations: classpath:db/migration
validate-on-migrate: true
validate-migration-naming: true
clean-disabled: true
baseline-on-migrate: false不要同时让 Hibernate ddl-auto=update、schema.sql 和 Flyway 修改同一套 schema。多个初始化器竞争 ownership 后,即使某次启动成功,也无法从 Git 历史还原对象究竟由谁创建。Spring Boot 数据库初始化说明也建议使用高层迁移工具时由它单独承担 schema 初始化。
应用启动迁移适合单体、小团队和低风险内部系统:部署单元少,数据库变更短,启动失败可以阻止错误版本接流量。Flyway 会利用数据库锁协调多个实例,但每个扩容实例仍要连接数据库、解析 migration、等待锁并验证历史。长 DDL 会把应用启动时间、平台 readiness timeout 和数据库变更窗口绑在一起。
核心系统更适合把迁移拆成独立 pipeline job 或 Kubernetes Job:唯一执行器先完成 validate、备份/恢复点确认和 migration,再允许应用灰度。不要把迁移放进每个 Pod 的 init container;十个副本同时启动虽然通常会被锁串行化,却会制造锁等待、连接压力和失败噪声。发布作业使用 DDL owner,应用运行账号只保留 DML 权限,两种凭据分别轮换和审计。
容器与 Compose 要让迁移成为一次性作业
本地联调可把数据库和 Flyway 写进 Compose,但 Flyway 服务应是执行后退出的一次性 job,不是常驻守护进程:
services:
db:
image: "postgres:${APPROVED_DB_VERSION:?set APPROVED_DB_VERSION}"
environment:
POSTGRES_DB: catalog
POSTGRES_USER: flyway_lab
POSTGRES_PASSWORD: ${LAB_PASSWORD:?set LAB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U flyway_lab -d catalog"]
interval: 2s
timeout: 2s
retries: 30
volumes:
- flyway-db-data:/var/lib/postgresql/data
migrate:
image: "redgate/flyway:${APPROVED_FLYWAY_VERSION:?set APPROVED_FLYWAY_VERSION}"
command: ["migrate"]
environment:
FLYWAY_URL: jdbc:postgresql://db:5432/catalog
FLYWAY_USER: flyway_lab
FLYWAY_PASSWORD: ${LAB_PASSWORD:?set LAB_PASSWORD}
FLYWAY_CLEAN_DISABLED: "true"
FLYWAY_VALIDATE_MIGRATION_NAMING: "true"
volumes:
- ./db/migration:/flyway/sql:ro
depends_on:
db:
condition: service_healthy
volumes:
flyway-db-data:运行与清理顺序为:
docker compose up -d db
docker compose run --rm migrate info
docker compose run --rm migrate migrate
docker compose run --rm migrate validate
docker compose down -v挂载 migration 为只读,避免容器内工具修改版本库;密码通过本地 .env 或 secret store 注入,.env.example 只保留变量名。down -v 会删除实验数据卷,只能在明确的本地 Compose 项目执行。共享数据库不要复用这条清理命令,也不要让 Compose project name 与其他项目碰撞。
CI 先证明“能从零构建”,发布作业再证明“能从当前状态升级”
一条只对开发者现有数据库执行 validate 的流水线会漏掉累计脚本错误。CI 至少建立两类证据:
空数据库重放:从空库执行全部 migration,运行应用冒烟,再销毁数据库。升级路径:从受支持的上一发布基线或脱敏快照执行新增 migration,验证耗时、锁、数据转换和应用兼容。
通用流水线骨架可以写成:
set -euo pipefail
flyway info -outputType=json > flyway-info-before.json
flyway validate -outputType=json > flyway-validate.json
flyway migrate -outputType=json > flyway-migrate.json
flyway validate -outputType=json > flyway-validate-after.json
./scripts/schema-smoke-test.sh
./scripts/assert-old-and-new-app-compatible.shCI 的 Flyway 发行物必须锁定版本或镜像 digest,数据库镜像也锁定团队支持版本。凭据来自受保护 secret,日志禁止 set -x,作业仅能访问临时库或目标发布库。PR 流水线绝不能持有生产 DDL 凭据;生产发布环境应要求受保护分支、审批、环境锁和不可变制品摘要。
流水线门禁不以“命令退出 0”结束,还要归档 migration 文件摘要、Flyway/driver 版本、目标环境标识、执行前后 info、数据库侧 schema 断言、耗时和发布关联号。对象定义可能包含敏感业务名,归档权限和保留期由审计要求决定。
并发锁只能串行执行,不能替团队决定谁有发布权
Flyway 在迁移开始时尝试获取数据库锁,阻止两个执行器并行修改 history 和 schema;获取失败会按 lockRetryCount 每隔 1 秒重试,默认次数也可能随发行线调整,团队配置不应依赖默认值。锁重试配置还规定 -1 表示无限等待。无限等待会让流水线看起来“还在运行”,实际可能是另一个发布器卡在长 DDL 或连接已经失控。
团队应给锁等待设置有限预算,并把超时视为发布冲突:查询正在运行的 Flyway job、数据库活动会话、DDL 锁和 history 最新记录,确认 owner 后再终止。不要看到锁等待就删除 history table 或杀掉数据库会话;非事务 DDL 被中断后可能已经留下部分对象。
应用启动迁移、独立流水线和人工 CLI 三个入口不能同时拥有默认发布权。推荐规则是:
生产只有一个受控 pipeline identity 可以执行 DDL。应用运行身份没有 ALTER、DROP、CREATE 权限。人工 CLI 只用于诊断;紧急执行必须获得临时凭据并关联审批记录。
Native Connector 等不具备相同锁行为的执行路径,不能沿用 JDBC 模式的并发假设。
多人并行修改同一 schema 时,数据库锁只能解决“执行时并发”,解决不了“设计时冲突”。两个 migration 分别重命名同一列、增加互斥约束,即使版本号不同也可能按顺序失败。合并门禁应在临时库重放合并后的完整主干,并对受影响对象做 ownership review。
失败恢复先判断数据库是否支持 DDL 事务
Flyway 默认尽可能把每个 migration 放进独立事务,但数据库对 DDL 事务的支持并不一致。PostgreSQL 等数据库能回滚多数事务内 DDL;MySQL、Oracle 的许多 DDL 会隐式提交。某些 PostgreSQL 或 SQL Server 语句又必须在事务外执行。group=true 尝试把本轮全部待执行 migration 放进一个事务,前提是数据库和整批脚本都支持;否则事务型与非事务型 migration 不能混跑。mixed=true 只为 PostgreSQL、Aurora PostgreSQL、SQL Server、SQLite 这类既有事务型又有禁止进入事务语句的数据库放开混合执行,不适用于用隐式提交处理 DDL 的 MySQL、Oracle,也不等于失败后拥有全局原子性。
故障现场按以下顺序取证:
flyway info -outputType=json > incident-info.json
flyway validate -outputType=json > incident-validate.json || true然后在数据库侧检查失败脚本涉及的表、索引、约束、触发器与数据行数,并保存数据库错误码和锁等待证据。若 DDL 已自动回滚,修复尚未进入永久下游的 migration 后在隔离库重试;若部分对象已提交,先按脚本逐条确认中间状态,人工撤销残留或从恢复点还原,再执行 repair 移除 failed 历史,最后重放。顺序不能倒过来,因为 repair 先成功只会让 Flyway 忘掉失败记录,数据库半成品仍然存在。
数据库回退通常优先前向修复,而不是机械生成逆向 SQL。删除列的逆操作可以重建列,却无法恢复列里的数据;缩小字段后再放大也找不回截断内容;数据修复脚本的逆向逻辑还可能覆盖迁移后产生的新业务数据。Flyway 的 U 类 undo migration 和 undo 命令属于 Teams 版本能力;它假定对应 versioned migration 已完整成功,仍无法解决非事务 migration 在任意语句处失败的半完成状态。可恢复性最终依赖经过演练的备份、PITR 或存储快照,以及明确的 RPO/RTO。
expand-contract 让应用回滚不依赖 schema 倒退
零停机变更的关键不是让 Flyway “更快”,而是让新旧应用在一段时间内共享兼容 schema。把重命名 customer.display_name 为 full_name 直接写成一次 DDL,会让旧代码立刻失效。更稳健的 expand-contract 分为多个独立发布:
Expand:新增可空 full_name,保留 display_name;先确认 DDL 锁影响和复制延迟。双写与回填:新应用同时写两列;后台任务分批回填历史数据,记录游标、失败批次与吞吐。切读:新应用读取 full_name,旧应用仍能读取旧列;对账两列差异并观察完整业务周期。
收紧:数据一致后增加约束或 NOT NULL。大表先使用数据库支持的低锁验证路径,避免一次扫描阻塞写入。Contract:所有受支持应用版本都不再依赖旧列后,单独 migration 删除旧列;删除前再次确认备份和依赖扫描。
索引创建、约束验证、枚举修改和大表回填必须按目标数据库设计。CREATE INDEX CONCURRENTLY 之类语句可能要求事务外执行;MySQL 在线 DDL 仍可能等待 metadata lock;回填事务过大会增加 WAL/binlog、复制延迟、undo 和锁持有时间。migration 只负责声明结构阶段,长时间数据回填更适合可暂停、可限速、可重试、可观测的独立作业。Flyway 版本记录中保留“创建回填条件”和“最终收口”,回填进度放在专用状态表或作业系统,不要伪装成一个数小时不透明 SQL。
常见失败要从第一条证据分型
“没有找到 migration”
先看 locations、容器挂载路径和 classpath 打包结果,再看文件名。非法命名在未启用 validateMigrationNaming 时可能只产生警告并被忽略。CI 应开启该配置,并检查构建产物中实际包含 db/migration。
“checksum mismatch”
对比已发布制品与当前文件的字节、换行、编码和 placeholder;确认脚本是否进入永久下游。恢复原文件通常比 repair 更正确。若变更确属审批后的历史校正,先证明数据库实际状态与新 checksum 对应,再由独立角色执行 repair。
“resolved migration not applied” 或 “out of order”
这通常来自长期分支晚合并、紧急热修版本插入或 locations 不一致。不要立即打开 outOfOrder。先在空库和目标版本克隆上比较执行顺序;优先把尚未发布的冲突脚本重命名为新的最高版本。允许 out-of-order 后,生产的实际执行顺序与空库重建顺序可能不同,必须用结构 diff 证明结果等价。
“schema 非空但没有 history table”
这可能是首次接管,也可能连错了数据库或 defaultSchema 配置错误。先核对目标身份、对象清单和 owner,再决定显式 baseline。禁止用 baselineOnMigrate 让错误目标自动通过。
“等待锁直到发布超时”
检查是否有另一个 Flyway job、应用启动迁移或人工 CLI;再查数据库 DDL/metadata lock。lockRetryCount 只控制 Flyway 获取迁移锁的等待,不会缩短 DDL 自己的锁等待。终止会话前先判断脚本是否事务化和已执行到哪条语句。
“repair 成功但再次 migrate 仍失败”
说明数据库真实对象没有清干净、repair 使用了不同 locations,或者修复后的脚本仍不兼容当前中间状态。回到数据库对象和错误码,不要连续 repair。必要时从隔离克隆复现相同半完成状态,验证清理 SQL 后再处理共享环境。
clean 必须从权限、配置和环境三层同时封死
clean 会删除配置 schema 中的对象,是开发环境重建工具,不是回滚命令。Flyway commands把它明确标为 development only。当前发行线的 cleanDisabled 默认值虽然是 true,团队仍要显式配置并验证,避免升级、环境覆盖或独立配置改变保护。仅靠“大家记得别运行”不构成保护。
第一层是配置:共享、测试、预发和生产固定 cleanDisabled=true,并阻止命令行或环境变量覆盖。第二层是权限:生产 Flyway identity 即使需要创建和修改业务对象,也不应拥有删除整个数据库、删除非目标 schema 或越过数据库边界的权限。第三层是环境:发布网络、账号、数据库名和 schema 白名单由平台控制,PR job 根本拿不到共享环境凭据。
本地反向实验应证明保护真实生效:
flyway clean预期命令非零退出并显示 clean disabled,随后 flyway info 和数据库对象仍然存在。若 clean 成功,立即停止推广这份配置,检查是不是命令行参数、环境变量或环境覆盖把 cleanDisabled 改成了 false。
开发者确实需要重建隔离库时,优先销毁专属容器/数据库并重新创建,而不是给通用 Flyway 配置打开 clean。若必须使用 clean,使用独立配置文件、随机数据库名、低权限账号和环境断言;命令执行前要求目标 host 为本机或临时命名空间,并禁止在共享 runner 上保存该配置。
团队把数据库变更当作制品而不是聊天记录
成熟的 Flyway 流程需要明确 ownership:业务开发提出 schema 与数据变化,数据库 owner 审查锁、容量、复制和恢复,应用 owner 证明新旧版本兼容,发布平台持有执行身份,安全负责人管理凭据与审计,值班人员维护失败恢复入口。一个人可以兼任多个角色,但生产变更不能由无人负责的共享账号完成。
每个 migration 的评审至少追问这些具体问题:
目标对象当前有多少数据、增长趋势怎样,DDL 是否扫描或重写整表。会取得什么锁,最长允许等待多久,超时和取消后留下什么状态。新旧应用、读副本、CDC、报表、搜索同步和数据导出是否依赖旧结构。
数据修复是否幂等、可限速、可断点续跑,失败批次如何识别。执行账号需要哪些最小权限,日志与制品是否暴露对象名或连接信息。应用回滚时 schema 是否仍兼容;真正的数据恢复依赖哪个恢复点。
migration 和 driver 版本由谁升级,baseline migration 由谁验证两条重放路径。
容量和成本也会进入工具决策。当前能力矩阵中,Community 足以完成 migrate、info、validate、repair、baseline、clean 等基础迁移链;undo 与 dry-run 能力位于 Teams,schema diff、迁移生成、漂移检测和集中治理中的部分能力位于 Enterprise。具体命令和配置项的层级会随发行线调整,版本升级或采购前应以目标发行版的 commands 能力表 为准,不能把某个版本的付费能力写成长期架构承诺。即使购买高级能力,也不能替代目标数据库的锁评估、备份恢复与应用兼容设计。
长期观测不只看“发布成功率”。更有用的趋势包括 migration 执行耗时相对历史基线的变化、锁等待年龄、失败后人工修复次数、checksum 漂移次数、环境 schema 差异、回填积压、应用兼容窗口长度和恢复演练结果。阈值来自目标表容量、发布 SLO 与数据库基线,不用脱离业务负载的万能秒数。
上线前把证据串成闭环
开发者提交前,应能从空库重放全部 migration,证明 repeatable 可重复执行,故意修改已执行脚本时 validate 必须失败,并确认本地销毁动作不会触达共享库。
评审合并前,应解决版本冲突,检查危险 DDL、数据回填、权限与兼容窗口;新增 baseline migration 时同时重放“新库基线”和“旧库升级”两条路径。
发布执行前,应核对目标身份、Flyway/driver/数据库版本、migration 摘要、备份或恢复点、锁预算、应用回滚兼容性和唯一执行器;生产配置必须保持 cleanDisabled=true、baselineOnMigrate=false,凭据不得进入命令行和日志。
执行失败后,应先保存 info、validate、数据库错误码、锁和真实对象状态,再按 DDL 事务能力选择回滚、人工清理或恢复。只有数据库状态已经和 migration 重新对齐,才考虑 repair;恢复完成后必须在隔离库复现并再次走完整升级链。
Flyway 的价值不在于替团队“自动执行 SQL”,而在于把每次数据库变化变成可排序、可验证、可追责的输入。真正可靠的发布仍依赖不可变 migration、隔离重放、单一执行权、兼容性演进和经过演练的数据恢复。五条链同时存在,数据库变更才从个人操作升级为团队工程能力。
