一、迁移为什么必须进流水线
手工执行迁移的代价集中在五点:无法重复,换个人执行顺序不同结果不同;无法审计,谁在什么时候改了什么只有聊天记录;无法回放,新环境搭起来时不知道要跑哪些脚本;环境漂移,测试库与生产库的 schema 悄悄不一致;无预演,语法错误在上线当刻才暴露。
把迁移纳入流水线后,schema 变更与代码变更走同一条路径:进 Git、过评审、在测试环境验证、在生产环境受审批约束执行。
四条铁律
1) 迁移脚本一旦合并就不可修改,只能追加新脚本
校验和机制会直接拒绝已执行过的脚本被改动
2) 每次执行前必须能回答「当前版本是多少」
Flyway 看 flyway_schema_history
Liquibase 看 DATABASECHANGELOG
Prisma 看 _prisma_migrations
3) 生产执行前必须有备份或快照
没有回滚方案的迁移不允许上生产
4) 迁移与应用部署解耦
先迁移后部署代码,且迁移必须向前兼容(见第五章)
流水线位置
开发阶段 本地跑迁移,验证脚本可执行
PR 阶段 CI 起临时数据库,从零执行全部迁移并校验
预发阶段 对预发库执行迁移,跑集成测试
生产阶段 审批后执行,执行前备份,执行后校验
二、Flyway 在 Actions 中的用法
2.1 基本工作流
name: Database migration
on:
push:
branches: [main]
paths:
- "migrations/**"
jobs:
migrate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install Flyway
run: |
curl -fsSL https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/10.17.0/flyway-commandline-10.17.0-linux-x64.tar.gz \
| tar xz
echo "$PWD/flyway-10.17.0" >> "$GITHUB_PATH"
- name: Migrate and validate
run: |
flyway -url=jdbc:postgresql://localhost:5432/app \
-user=postgres -password=postgres \
-locations=filesystem:./migrations \
migrate
flyway -url=jdbc:postgresql://localhost:5432/app \
-user=postgres -password=postgres \
-locations=filesystem:./migrations \
validate
2.2 版本化与可重复迁移
V1__create_users.sql 版本化迁移,只执行一次
V2__add_email_index.sql 版本号递增,禁止跳号
R__refresh_views.sql 可重复迁移,校验和变化时重跑
U2__undo_email_index.sql undo 迁移(社区版需插件)
-- migrations/V2__add_email_index.sql
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email
ON users (email);
命名规范要点
1) 版本号唯一,重复版本号会导致 migrate 直接失败
2) 双下划线 __ 分隔版本与描述,单下划线会被当成版本的一部分
3) 生产环境禁止用 U 开头的 undo 脚本回滚,回滚应写新迁移
2.3 flyway_schema_history 与常见故障
SELECT installed_rank, version, description, type,
script, checksum, installed_on, execution_time, success
FROM flyway_schema_history
ORDER BY installed_rank;
checksum mismatch
原因:已执行过的脚本被人改了
处理:还原脚本,或用 flyway repair 重建校验和(慎用)
failed migration
原因:脚本执行到一半报错
处理:手工清理残留对象,删除 success=false 的行,再 repair
out of order
原因:补了一个低版本号脚本
处理:默认报错,可开启 outOfOrder=true,但会破坏可重现性
baselineOnMigrate 用于把已有数据库纳入 Flyway 管理:首次执行时把当前状态记为基线版本 0,之后的迁移从 1 开始。没有它,第一次 migrate 会因为「库非空且无历史表」而直接报错。
flyway -url="$DB_URL" -user="$DB_USER" -password="$DB_PASS" \
-baselineOnMigrate=true -baselineVersion=0 migrate
三、Liquibase 在 Actions 中的用法
3.1 changelog 与 changeSet
# changelog/db.changelog-master.yaml
databaseChangeLog:
- include:
file: changelog/001-init.yaml
- include:
file: changelog/002-add-email-index.yaml
# changelog/002-add-email-index.yaml
databaseChangeLog:
- changeSet:
id: 002-add-email-index
author: leeting
changes:
- createIndex:
indexName: idx_users_email
tableName: users
unique: true
columns:
- column:
name: email
rollback:
- dropIndex:
indexName: idx_users_email
tableName: users
Liquibase 与 Flyway 的差异
1) changeSet 有 id + author 组成的唯一标识,天然支持乱序
2) 每个 changeSet 可声明 rollback,回滚语义内建
3) 支持 XML / YAML / JSON / SQL 四种格式
4) 与数据库无关的抽象 change 可跨库
3.2 updateSQL 预演
updateSQL 不执行,只输出将要执行的 SQL,这是 CI 中最有价值的一步:
- name: Liquibase preflight
run: |
liquibase \
--url="$DB_URL" --username="$DB_USER" --password="$DB_PASS" \
--changelog-file=changelog/db.changelog-master.yaml \
--output-file=planned.sql \
updateSQL
cat planned.sql
- name: Upload planned SQL
uses: actions/upload-artifact@v4
with:
name: planned-migration-sql
path: planned.sql
预演的价值
1) PR 里可以直接看到将要执行的原生 SQL
2) 可以写规则检查危险语句
3) 可以人工在评审时确认执行计划
3.3 危险语句检查与打标签
set -euo pipefail
fail=0
if grep -Ei '\bDROP[[:space:]]+(TABLE|COLUMN)\b' planned.sql; then
echo "::error::检测到 DROP 语句,需要人工确认"; fail=1
fi
if grep -Ei '\bUPDATE\b' planned.sql | grep -vEi '\bWHERE\b'; then
echo "::error::检测到无 WHERE 的 UPDATE"; fail=1
fi
exit $fail
liquibase --url="$DB_URL" --username="$DB_USER" --password="$DB_PASS" \
--changelog-file=changelog/db.changelog-master.yaml \
--tag=release-${{ github.run_number }} \
update
打 tag 之后可以用 rollback --tag=release-41 回滚到指定标签,这是 Liquibase 相对 Flyway 的显著优势。
四、Prisma Migrate 在 Actions 中的用法
4.1 migrate dev 与 migrate deploy
prisma migrate dev
用途:本地开发
行为:对比 schema 与数据库差异,生成新迁移文件,执行迁移
附带:重建 shadow database,触发 prisma generate
禁止:在 CI 与生产使用(会尝试生成新迁移、可能重置数据库)
prisma migrate deploy
用途:CI 与生产
行为:只执行 migrations 目录中尚未应用的迁移
不生成:任何新迁移文件
不重置:任何数据
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Apply migrations
run: |
npm ci
npx prisma generate
npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
4.2 shadow database
shadow database 是什么
Prisma 需要一个额外的空库来「重放全部迁移」,
以推断当前 schema 应该长什么样,从而判断是否有漂移。
本地开发
Prisma 自动创建与销毁,需要数据库用户有 CREATEDB 权限
CI 中
1) 用 services 起一个 Postgres,权限足够即可
2) 或用云提供的 shadow database URL
3) 托管数据库通常禁止建库,需要显式指定 shadowDatabaseUrl
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
4.3 漂移检测与基线
npx prisma migrate status
npx prisma migrate diff \
--from-schema-datasource prisma/schema.prisma \
--to-schema-datamodel prisma/schema.prisma \
--script > migrations/20261006000000_baseline/migration.sql
npx prisma migrate resolve --applied 20261006000000_baseline
常见错误码
P3005 数据库非空但无迁移历史 → 用 migrate resolve 打基线
P3018 迁移执行失败 → 看 error 字段定位语句,修复后 resolve
P3009 存在失败的迁移 → 必须 resolve 后才能继续
P1010 权限不足 → 检查 DATABASE_URL 里的用户权限
维度 Flyway Liquibase Prisma Migrate
脚本形式 SQL 为主 XML/YAML/SQL schema.prisma 生成
校验机制 checksum changeset 哈希 checksum
回滚 undo 脚本 rollback 内建 不内建,靠前滚
预演 dryRun 有限 updateSQL 强 migrate diff
适用 JVM 技术栈 企业多库场景 Node 全栈
五、向前兼容的迁移写法
数据库迁移最大的风险是「代码与 schema 版本不同步的窗口期」。在这个窗口里,旧版本代码仍在运行,新 schema 必须同时兼容两边,这就是 expand-contract 模式要解决的问题。
以「把 users.name 拆成 first_name 与 last_name」为例
阶段 1 expand(只加不删)
迁移:新增 first_name、last_name 两列,允许 NULL
代码:双写,读时优先读新列,回退读旧列
此时新旧代码都能正常工作
阶段 2 migrate(回填数据)
迁移:UPDATE 把 name 拆分写入新列(分批,避免长事务)
代码:仍然双写,读走新列
阶段 3 contract(删除旧列)
确认无任何代码引用 name 后再执行
迁移:ALTER TABLE users DROP COLUMN name
代码:移除双写逻辑
三个阶段的部署之间要隔足够时间(通常一个发布周期以上),确保回滚时不会因为列已删除而失败。
-- 好:加列带默认值,Postgres 11+ 不会重写全表
ALTER TABLE orders ADD COLUMN status text NOT NULL DEFAULT 'pending';
-- 危险:先加可空列再补默认值,第二步会重写全表
ALTER TABLE orders ADD COLUMN status text;
UPDATE orders SET status = 'pending';
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;
大表分步执行建议
1) 先加可空列,代码双写
2) 分批回填:UPDATE ... WHERE id BETWEEN x AND y LIMIT 10000,循环
3) 加 CHECK 约束时用 NOT VALID,再 VALIDATE CONSTRAINT 避免长锁
4) 确认无 NULL 后再 SET NOT NULL
-- 好:并发建索引,不阻塞写
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_user ON orders (user_id);
锁风险速查
操作 锁级别 风险
ADD COLUMN(有默认值,PG11+) ACCESS EXCLUSIVE 瞬时
CREATE INDEX SHARE 阻塞写
CREATE INDEX CONCURRENTLY 无长锁 推荐,失败会留无效索引
SET NOT NULL ACCESS EXCLUSIVE 全表扫描
ADD FOREIGN KEY SHARE ROW EXCLUSIVE 需验证全表
ALTER COLUMN TYPE ACCESS EXCLUSIVE 常需重写全表
并发建索引失败会留下 INVALID 状态的索引,必须手动 DROP 后重建,否则后续同名创建会报已存在。顺序上永远先迁移后部署代码,且迁移必须向后兼容。
六、CI 中的 dry-run 与 SQL 审查
6.1 从零执行全部迁移
- name: Apply all migrations from scratch
run: |
for f in migrations/V*.sql; do
echo "applying $f"
psql -h localhost -U postgres -d app -v ON_ERROR_STOP=1 -f "$f"
done
env:
PGPASSWORD: postgres
- name: Verify schema shape
run: psql -h localhost -U postgres -d app -c '\d+ users'
env:
PGPASSWORD: postgres
从零重放能暴露「依赖了某个手工创建的对象」这类隐藏问题,这是迁移流水线最有价值的检查之一。
6.2 危险操作审查门禁
- name: Block destructive statements
run: |
set -euo pipefail
if grep -rEn 'DROP[[:space:]]+(DATABASE|SCHEMA)' migrations/; then
echo "::error::迁移中禁止出现 DROP DATABASE / DROP SCHEMA"; exit 1
fi
if grep -rEn 'TRUNCATE' migrations/; then
echo "::error::迁移中出现 TRUNCATE,请改用带条件的 DELETE"; exit 1
fi
建议纳入门禁的规则
1) 禁止 DROP DATABASE / DROP SCHEMA
2) 禁止无 WHERE 的 UPDATE / DELETE
3) 大表 DROP COLUMN 必须带 review 标签
4) ALTER COLUMN TYPE 必须单独一个 PR
5) 所有迁移文件必须能通过 SQL 解析(sqlfluff / pg_format)
七、生产迁移的审批与备份
7.1 环境门禁
migrate-prod:
needs: verify-migrations
runs-on: ubuntu-latest
environment:
name: production-db
steps:
- uses: actions/checkout@v4
- name: Snapshot before migration
run: |
aws rds create-db-snapshot \
--db-instance-identifier app-prod \
--db-snapshot-identifier pre-migrate-${{ github.run_number }}
aws rds wait db-snapshot-completed \
--db-snapshot-identifier pre-migrate-${{ github.run_number }}
- name: Apply migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
environment: production-db 上的 required reviewers 让 job 进入等待状态,只有审批人批准后才继续,这比「靠人记得点确认」可靠得多。
7.2 备份策略与迁移后校验
备份策略选择
快照(RDS/Aurora) 秒级到分钟级创建,恢复需数分钟到数十分钟
逻辑备份(pg_dump) 耗时与库大小成正比,恢复慢但可单表恢复
时间点恢复(PITR) 持续归档,可恢复到任意秒
生产迁移的最低要求
1) 迁移前创建快照并等待完成,不能只发起不等待
2) 快照 ID 写进 job summary,便于事后定位
3) 记录迁移前的 schema 版本与历史表快照
4) 大表变更额外做一次逻辑备份
- name: Record pre-migration state
run: |
psql "$PROD_DATABASE_URL" -c '\dt' > pre-tables.txt
psql "$PROD_DATABASE_URL" -c 'SELECT * FROM flyway_schema_history' > pre-history.txt
{
echo "### 迁移前状态"
echo "snapshot: pre-migrate-${{ github.run_number }}"
} >> "$GITHUB_STEP_SUMMARY"
env:
PROD_DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
迁移后校验清单
[ ] 迁移状态为 up to date
[ ] 关键表结构与预期一致
[ ] 关键表行数没有异常减少
[ ] 应用健康检查通过,错误率没有上升
八、失败回滚策略
8.1 回滚的三个层次
层次 1:迁移未完成就失败
处理:修复脚本,手工清理残留对象,repair 后重跑
注意:DDL 在多数数据库里不支持事务回滚(MySQL 尤其),
部分成功是常态,必须人工确认残留状态
层次 2:迁移成功但应用出问题
处理:优先前滚修复(再写一个迁移),而非回滚数据库
原因:回滚数据库可能丢数据,且旧版本代码未必兼容
层次 3:必须回滚数据库
处理:从快照恢复,或执行预先写好的反向迁移
代价:恢复期间服务不可用,且会丢失快照之后的数据
8.2 前滚优先原则
为什么优先前滚
1) 迁移已应用的列可能已被新数据写入,回滚会丢数据
2) 快照恢复需要停机,且时间与库大小成正比
3) 反向迁移本身也可能出错,风险叠加
前滚的做法
1) 写一个修复迁移(V_next__fix_xxx.sql)
2) 走同一条流水线,同样受门禁约束
3) 如果问题是性能而非正确性,考虑加索引或改查询
8.3 回滚预案模板
迁移编号:V42__add_orders_region.sql
变更内容:orders 表新增 region 列(可空)
影响范围:orders 表约 800 万行
锁风险:ADD COLUMN 有默认值,PG16 瞬时完成
执行时长预估:小于 1 秒
备份:pre-migrate-1042 快照已创建
回滚方案:ALTER TABLE orders DROP COLUMN region(无数据依赖,可安全回滚)
观察指标:orders 写入延迟、应用错误率
超时阈值:5 分钟内错误率超过 1% 则触发回滚
自动回滚只应覆盖「明确安全」的操作,删除列、恢复快照这类动作必须留人工确认,否则一次误判会把小故障变成数据事故。
总结
数据库迁移流水线的价值不在于自动化执行,而在于把不可逆的 schema 变更纳入可重复、可审计、可预演的流程。Flyway 用版本化脚本与 flyway_schema_history 提供最朴素的保证,baselineOnMigrate 解决存量库接入;Liquibase 用 changeSet 的 id 与 author 支持乱序,用 updateSQL 把将要执行的 SQL 摊开在 PR 里供人审查,用 tag 支持按发布批次回滚;Prisma Migrate 则要牢记 migrate dev 只属于本地、CI 与生产一律 migrate deploy,并用 shadow database 与 migrate diff 处理漂移与基线。写法上遵循 expand-contract,加列不删列、双写过渡、分步加约束,才能让新旧代码在切换窗口里共存。最后用危险语句门禁、审批环境、快照前置与前滚优先的回滚策略兜底,数据库这条最容易出事故的链路才算真正被管住。
延伸阅读:
- GitHub Actions 与 Kubernetes GitOps 部署 — 部署编排与回滚
- GitHub Actions Environment 审批门禁 — 生产发布审批配置
- GitHub Actions 安全加固 — 密钥与权限最小化
- GitHub Actions 与 Terraform 基础设施即代码 — 基础设施变更的同类门禁
- GitHub Actions 测试与覆盖率集成 — 集成测试与数据库服务
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。