代码可以随时回滚、随时重建,数据库却不行——一次 Schema 变更在生产库上跑挂了,代价远超一次发布失败。传统"DBA 手工执行迁移脚本"的方式,既慢又不可控。数据库变更的 DevOps 工程化,就是把迁移当成代码的一部分:进 Git、过 CI、可验证、可回滚、按序演进,并配合 Expand-Contract 兼容策略,让数据库变更与代码发布安全解耦。
目录
- 1. 为什么数据库变更要 DevOps 化
- 2. 迁移工具的两种范式:Flyway vs Liquibase
- 3. 把迁移接进 CI/CD
- 4. 迁移的可验证性:空库/升级库/回滚
- 5. Expand-Contract:兼容变更的核心策略
- 6. 可回滚与影子库演练
- 7. 数据库与代码发布的解耦
- 8. 迁移流水线的门禁与审批
- 9. 生产最佳实践与避坑
1. 为什么数据库变更要 DevOps 化
1.1 手工迁移的三大问题
问题一:不可审计
谁在什么时候改了什么表?——手工执行无记录
问题二:不可复现
本地能跑、生产跑挂,没人能再现场复现
问题三:与代码脱节
代码需要"新列",DBA 却还不知道 → 上线炸
DevOps 化的目标:
迁移像代码一样:版本化、可测试、可回滚、进流水线
1.2 迁移进 CI 后的收益
- 每个 PR 带迁移 → 代码与 Schema 一起演进
- 迁移在测试库/影子库先验证 → 生产风险大幅下降
- 迁移有回滚脚本 → 出问题能安全后退
- 全部记录在 Git → 审计与复盘有据
2. 迁移工具的两种范式:Flyway vs Liquibase
2.1 Flyway(命令式脚本,SQL 优先)
-- V1__create_orders.sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
status VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
-- V2__add_paid_at.sql
ALTER TABLE orders ADD COLUMN paid_at TIMESTAMP NULL;
flyway migrate -url=... -user=... -password=...
# 自动记录到 flyway_schema_history:版本、校验和、成功/失败
2.2 Liquibase(声明式 Changelog)
<!-- changelog.xml:声明式定义变更,跨数据库一致性 -->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
<changeSet id="1" author="me">
<createTable tableName="orders">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true"/>
</column>
<column name="status" type="varchar(20)">
<constraints nullable="false"/>
</column>
</createTable>
<rollback>
<dropTable tableName="orders"/>
</rollback>
</changeSet>
</databaseChangeLog>
2.3 选型速览
| 维度 | Flyway | Liquibase |
|---|---|---|
| 风格 | 命令式 SQL 脚本 | 声明式 XML/YAML/SQL |
| 学习曲线 | 低(就是 SQL) | 中(需学 DSL) |
| 跨库一致性 | 靠 SQL 自己 | 声明式自动适配 |
| 回滚 | 需手写 undo 脚本 | rollback 块可声明 |
| 生态 | Spring 默认 | 大型平台更友好 |
ℹ️ 选择:团队熟 SQL → Flyway;多数据库/需要更强声明控制 → Liquibase。两者都能进 CI/CD,核心是"版本化 + 可验证 + 可回滚"。
3. 把迁移接进 CI/CD
3.1 迁移作为流水线的一等公民
# GitHub Actions:PR 跑迁移验证,合并后再跑生产
jobs:
migrate-test:
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: test }
steps:
- run: flyway migrate -url=${{ job.services.postgres.url }} # 空库迁移验证
- run: pytest tests/data_layer # 在迁移后的库上跑数据层测试
migrate-prod:
if: github.ref == 'refs/heads/main' # 合并/发布时跑生产迁移
environment: production
steps:
- run: flyway migrate -url=${{ secrets.PROD_DB_URL }}
关键:迁移在"测试库先验证、生产库晚执行"
与代码发布脱耦(见第 7 节),避免"代码已发、迁移未跑"的错位
3.2 迁移脚本的命名纪律
命名规范(决定版本顺序与可追踪性):
Flyway: V<版本>__<描述>.sql
例:V1__create_orders.sql、V2__add_paid_at.sql
Liquibase: changeSet 的 id + author 唯一标识
铁律:
- 已合并的迁移脚本不可修改(校验和会被发现)
- 新变更永远追加新版本,而不是改写旧版本
- 多环境顺序一致:dev→test→prod 同一套脚本
4. 迁移的可验证性:空库/升级库/回滚
4.1 三类迁移测试
1. 空库安装(fresh install)
从零库一路 migrate 到最新 → 验证全新环境可用
2. 升级路径(upgrade)
从 V(N) 状态升级到最新 → 验证已有库平滑演进
3. 回滚验证(rollback)
从新状态回退 → 验证回滚脚本正确
在 CI 里对每种数据库状态各跑一遍:
这比"只在开发机上跑一次"可靠得多
4.2 一个迁移验证套件
# migrate-test 套件(在 CI 的 Testcontainers 库上跑)
# 1) 空库安装
flyway migrate -target=latest # 空库一路到最新
# 2) 升级路径:先迁到 V2,再升级到最新
flyway migrate -target=V2
flyway migrate -target=latest
# 3) 回滚:模拟回退(Flyway 用 undo 脚本)
flyway undo -target=V2 # 若有 undo
# 4) 数据完好性校验:迁移前后 count/checksum 对比
./scripts/verify-after-migrate.sh
5. Expand-Contract:兼容变更的核心策略
5.1 为什么不能"一步到位改表"
一步到位的风险:
新代码依赖"新列" → 旧代码还在写 → 崩溃
加 NOT NULL 列 → 表中已有行没值 → 迁移失败
删列 → 旧代码还在读 → 报错
Expand-Contract(扩展-收缩)把"破坏性变更"拆成兼容的多步:
1. Expand(加兼容层):加新列(可空/默认)、保留旧列
2. 双写/迁移数据:新旧共存,应用逐步切换
3. Contract(收缩):旧代码下线后,才删旧列/收紧约束
5.2 一个 Expand-Contract 示例
例:把 status VARCHAR 改为 ENUM('PENDING','PAID','CANCELLED')
Step1(Expand):
ALTER TABLE orders
ADD COLUMN status_enum ENUM(...) NULL; -- 新列可空,不破坏旧代码
Step2(双写):
应用同时写 status 与 status_enum(新代码已上线)
后台批量回填 status_enum = 转换(status)
Step3(Contract):
确认所有读都走 status_enum 且旧代码下线后:
ALTER TABLE orders DROP COLUMN status;
ALTER TABLE orders MODIFY status_enum NOT NULL;
中间每一步都可发布、可回滚、可验证
ℹ️ 核心:数据库变更与代码发布是"两步走",Expand-Contract 保证任意时刻旧代码+新代码都能与 Schema 共存——这是安全演进的地基。
6. 可回滚与影子库演练
6.1 回滚不是"删掉脚本"
迁移的"回滚"指的是:
- 能恢复到迁移前的 Schema 与数据(undo/down 脚本)
- 或从备份恢复(更强的兜底)
回滚分级:
1. 脚本级:undo 脚本(改回旧结构)——优先
2. 数据级:先备份(dump/快照)再迁移——必做
3. 实例级:整库恢复备份——最后手段
纪律:每次迁移前先备份 → 迁移失败可恢复到"迁移前"
6.2 影子库演练(Shadow Database)
影子库 = 用生产数据拷贝在"隔离库"上先跑真实迁移
- 用生产备份/CDC 造一份影子库(脱敏)
- 在影子库上执行即将上线的迁移
- 观察:迁移耗时、锁等待、数据一致性、回滚路径
为什么有效:
迁移在"真实数据量/真实数据分布"下才可能暴露性能与一致性问题
影子库演练 = 把生产迁移的"第一次"提前到安全环境
7. 数据库与代码发布的解耦
7.1 两个独立发布通道
代码发布 ≠ 数据库变更:
- 代码:随时可回滚、可重建(K8s Deployment / 蓝绿)
- 数据库:Schema 演进是"单向进度"(无法随便回退版本)
解耦设计:
1. 迁移在"代码合并前"先安全应用(Expand 阶段可空/兼容)
2. 代码在"迁移已兼容"之后发布
3. 若代码出问题 → 只回滚代码(Schema 保持兼容状态)
4. Contract(删旧列)在旧代码完全下线后才做
顺序铁律:
先 Expand(兼容)→ 再发代码 → 后 Contract(收尾)
保证任何时刻"线上代码"与"库结构"兼容
7.2 与 CI/CD 的时序
推荐流水线:
PR → 迁移验证(测试库)→ 合并
↓
main → 生产库执行"Expand 阶段迁移"(可空/兼容)
↓
(验证兼容 OK)
main → 代码发布(依赖新结构的部分开始可用)
↓
(旧代码全部下线,观察稳定)
定时/发布后 → 执行"Contract 阶段迁移"(删旧列/收紧)
通过将迁移拆成"兼容"与"收紧"两阶段,与代码发布错峰
8. 迁移流水线的门禁与审批
8.1 迁移质量的强制门槛
迁移进 PR 的门禁(可在 CI 里强制):
- 语法/校验:Flyway validate(校验和一致)
- 空库/升级/回滚三套件必过
- 锁/耗时评估:大表 DDL 需人工审批
- 回滚脚本必须存在(无回滚的迁移默认拒绝)
- 数据库类型/兼容性检查(Expand 先于 Contract)
DBA 审批点(高影响变更):
- 大表 ALTER(加锁/耗时风险)
- 改类型 / 删列 / 收紧约束
- 跨团队共用的核心表
8.2 审批如何接进流程
用环境/分支保护实现"迁移审批门禁":
- GitHub:environment production 需要 reviewer 批准
- 或单独一个"migration-review" job,人工 approve 后才跑生产
关键:
- 迁移代码本身仍在 Git、仍过 CI
- 只是"生产执行"需要审批,不阻塞开发迭代
- 审批内容:影响评估(锁、数据、回滚路径)
9. 生产最佳实践与避坑
9.1 Checklist
□ 迁移进 Git + CI(空库/升级/回滚三套件必过)
□ 命名规范(Vx__desc),已合并脚本不可改
□ Expand-Contract 拆兼容/收紧两阶段
□ 迁移前必备份,配回滚/undo 脚本
□ 高影响迁移走影子库演练
□ 数据库变更与代码发布解耦(先 Expand 再发码)
□ 大表 DDL / 类型变更走人工审批
□ 监控迁移执行(锁、耗时、失败告警)
□ 迁移状态可审计(schema history 表)
9.2 常见坑
| 坑 | 现象 | 对策 |
|---|---|---|
| 迁移与代码同时发布 | 旧代码兼容不了新库 | 解耦 + Expand 先行 |
| 没有回滚脚本 | 出问题只能硬扛 | 每迁移配 undo |
| 迁移不验证 | 生产第一次跑就挂 | CI 三套件 + 影子库 |
| 改已合并脚本 | 校验失败 / 不一致 | 追加新版本 |
| 加 NOT NULL 列 | 存量行没值迁移失败 | 先可空 + 回填再收紧 |
| 大表直接 ALTER | 锁库/超时 | 评估 + 分批 + 审批 |
9.3 一句话原则
数据库变更的本质是"单向进化的 Schema 与多版本代码共存"
——用 Expand-Contract 让它们始终兼容。
小结
数据库变更的 DevOps 化 = 迁移进代码、CI 三套件验证、Expand-Contract 兼容演进、可回滚 + 影子库演练、与代码发布解耦、高影响变更走审批。落地记住五件事:迁移进 Git 过 CI、命名规范防改已合并脚本、Expand 先于 Contract、迁移前备份 + 配回滚、大表变更走审批。当 Schema 的每一次演进都在"安全网内"发生,数据库就从"上线的风险源"变成了"可预测、可审计、可回滚"的基础设施——这正是平台工程把数据库变成一等公民的意义。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。