Flyway:数据库版本管理实战——从入门到项目落地
大家好,我是黒漂技术佬。
今天聊一个很多团队"出事之后才想起来"的话题——数据库版本管理。
你有没有遇到过这种情况:开发环境数据库跑得好好的,一到测试环境就报Unknown column 'xxx'?或者新同事拉下代码跑不起来,因为少了一张表?再或者,上线的 SQL 脚本到底执行了没有,谁也说不清楚?
这些问题的根源都一样:代码有 Git 管版本,数据库脚本却是野生的。
Flyway 就是来解决这个问题的。
一、Flyway 是什么?
一句话:Flyway 是数据库的 Git。
它在数据库中维护一张flyway_schema_history表,记录每一次数据库变更的版本号、脚本名、校验和、执行时间。每次启动时,Flyway 扫描你的迁移脚本目录,和历史表一对比——没执行过的就执行,执行过但被篡改的就报错,干干净净。
┌──────────────────────────────────────────────────┐ │ Flyway 工作流程 │ │ │ │ 启动 → 扫描 migrations 目录 → 读取历史表 │ │ → 对比版本号 → 执行未执行的脚本 → 更新历史表 │ │ → 校验已有脚本完整性 → 应用启动 │ └──────────────────────────────────────────────────┘二、为什么需要 Flyway?
2.1 没有版本管理的痛苦
拿我们无人售货柜项目来说,数据库里有订单表、设备表、商品表、支付表等几十张表。后端是用 SpringBoot + MyBatis-Plus 做的,开发过程中表结构变更是家常便饭:
- 今天给
device表加个firmware_version字段 - 明天给
order表加个refund_reason字段 - 后天发现
goods表缺个索引,查询慢得要命
在没有 Flyway 的时候,流程是这样的:
“老张,我刚改了表结构,SQL 发你了,待会部署的时候跑一下。”
老张:“收到。”
三天后上线。用户反馈:货柜扫码不出商品。查了半天——老张的 SQL 跑在了测试库,生产库没跑。
这是真实发生过的事故。从那以后,我们所有的 SQL 变更都走 Flyway 管理。
2.2 Flyway 解决的核心问题
| 问题 | Flyway 如何解决 |
|---|---|
| SQL 脚本漏执行 | 启动自动扫描,未执行的自动补上 |
| 脚本重复执行 | 已执行的脚本不会重复跑 |
| 脚本被篡改 | 校验和(checksum)不一致直接报错 |
| 多人协作冲突 | 版本号唯一,冲突在开发阶段就能发现 |
| 环境不一致 | 同一套脚本,开发/测试/生产执行结果一致 |
| 回滚困难 | 配合 Undo 迁移(商业版),或手动写逆向脚本 |
三、核心概念:迁移脚本
Flyway 的迁移脚本有三种类型,靠文件名前缀区分:
3.1 版本化迁移(Versioned Migration)——V
这是最常用的类型,文件名格式:
V<版本号>__<描述>.sql注意:版本号后面的分隔符是两个下划线,不是单个。
-- V1__init_database.sqlCREATETABLEdevice(idBIGINTPRIMARYKEYAUTO_INCREMENT,device_codeVARCHAR(32)NOTNULLCOMMENT'设备编号',statusTINYINTDEFAULT0COMMENT'状态:0-离线 1-在线 2-故障',create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP)COMMENT'设备表';-- V2__add_firmware_version.sqlALTERTABLEdeviceADDCOLUMNfirmware_versionVARCHAR(16)DEFAULT'1.0.0'COMMENT'固件版本号';版本号规则:
- 数字递增,不能重复,不能跳号(跳号不会报错但你会后悔)
- 已执行的版本号绝对不能修改——改版本号等于告诉 Flyway “这是新脚本”,它会尝试再执行一遍
- 团队协作时,先到先得——谁先合代码谁占住版本号
3.2 撤销迁移(Undo Migration)——U
商业版才支持,文件名与对应的 V 脚本一一对应:
-- U2__remove_firmware_version.sqlALTERTABLEdeviceDROPCOLUMNfirmware_version;社区版没有这个功能,但可以通过写V脚本来做回滚操作——不推荐,因为版本号不可逆。社区版的最佳实践是:只向前,不后退。真要回滚就写一个新的V脚本把数据改回来。
3.3 可重复迁移(Repeatable Migration)——R
文件名格式:
R__<描述>.sql和版本化迁移最大的区别:内容变了就会重新执行。Flyway 用 checksum 来判断是否需要重新运行。
适用场景:
- 视图定义(CREATE OR REPLACE VIEW)
- 存储过程、函数
- 需要始终保持最新状态的触发器
-- R__device_overview_view.sqlCREATEORREPLACEVIEWdevice_overviewASSELECTd.device_code,d.status,d.firmware_version,COUNT(o.id)AStoday_ordersFROMdevice dLEFTJOIN`order`oONd.id=o.device_idANDDATE(o.create_time)=CURDATE()GROUPBYd.id;每次改这个视图定义,Flyway 检测到 checksum 变了,就会重新执行这个脚本。
四、flyway_schema_history:一切的核心
Flyway 在数据库中维护一张元数据表(默认叫flyway_schema_history),这张表就是一切魔法的基础。
CREATETABLE`flyway_schema_history`(`installed_rank`INTNOTNULL,-- 执行顺序`version`VARCHAR(50),-- 版本号(V 脚本有,R 脚本为 NULL)`description`VARCHAR(200)NOTNULL,-- 描述(文件名中的描述部分)`type`VARCHAR(20)NOTNULL,-- 类型:SQL / JDBC`script`VARCHAR(1000)NOTNULL,-- 脚本文件名`checksum`INT,-- 校验和`installed_by`VARCHAR(100)NOTNULL,-- 执行者(数据库用户名)`installed_on`TIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMP,`execution_time`INTNOTNULL,-- 执行耗时(毫秒)`success`TINYINT(1)NOTNULL-- 是否成功);每次启动,Flyway 做三件事:
- 扫描
classpath:db/migration目录下所有符合命名规则的.sql文件 - 对比文件版本号与
flyway_schema_history表中已记录的版本号 - 执行未执行的新脚本,并将执行结果写入历史表
如果发现已执行的脚本 checksum 与记录不一致 →直接抛异常,应用启动失败。这是故意的——Flyway 宁可让你启动不了,也不让数据出问题。
五、SpringBoot 集成 Flyway
SpringBoot 对 Flyway 的支持是开箱即用的,引入依赖,写好脚本,就完事了。
5.1 添加依赖
<!-- pom.xml --><dependency><groupId>org.flywaydb</groupId><artifactId>flyway-core</artifactId></dependency><dependency><groupId>org.flywaydb</groupId><artifactId>flyway-mysql</artifactId></dependency>SpringBoot 会自动配置 Flyway,因为 spring-boot-starter 里已经包含了对 flyway 的自动配置。你只需要确保flyway-core在 classpath 上就行。
5.2 放置迁移脚本
在src/main/resources/db/migration/目录下按序放置 SQL 文件:
src/main/resources/ └── db/ └── migration/ ├── V1__init_database.sql ├── V2__add_firmware_version.sql ├── V3__create_order_table.sql ├── V4__add_order_index.sql └── R__device_overview_view.sql5.3 配置文件
# application.ymlspring:flyway:enabled:truelocations:classpath:db/migration# 如果数据库不是空的(有历史数据),需要设置基线版本baseline-on-migrate:truebaseline-version:1# 禁止在事务中执行(MySQL DDL 不支持事务回滚)# 注意:新版 Flyway 默认根据数据库类型自动判断,一般不需要手动设# 编码encoding:UTF-8# 如果脚本校验和不匹配,是否允许启动(生产环境坚决设 false)validate-on-migrate:true重点解释几个参数:
baseline-on-migrate: true
这个是给已有数据库用的。假设你的项目已经跑了半年,数据库里有一堆表,现在要接入 Flyway。此时 Flyway 看到数据库中已有device表,而你的V1__init_database.sql脚本里恰好是CREATE TABLE device,这不就冲突了吗?
开启baseline-on-migrate后,Flyway 会把当前数据库状态标记为baseline-version(比如 V1),之后只执行 V2 及以后的脚本。已有的表 Flyway 不动,当它是 V1 的产物。
validate-on-migrate: true
启动时校验已执行脚本的 checksum 是否一致。生产环境必须开,不然有人偷偷改了脚本你还不知道。
5.4 程序化配置(高级用法)
如果需要在代码里动态控制 Flyway 行为:
@ConfigurationpublicclassFlywayConfig{@BeanpublicFlywayMigrationStrategyflywayMigrationStrategy(){returnflyway->{// 启动前先 repair(修复 checksum,慎用!)// flyway.repair();// 执行迁移flyway.migrate();// 打印迁移结果MigrationInfoServiceinfo=flyway.info();MigrationInfocurrent=info.current();if(current!=null){log.info("当前数据库版本:{}",current.getVersion().getVersion());}log.info("所有迁移脚本已执行");};}}六、实战踩坑与最佳实践
6.1 开发流程
团队开发中,正确的流程是:
- 拉最新代码(包括最新的 migration 脚本)
- 如果需要改数据库,创建新的
V脚本,版本号取当前最大版本号 +1 - 本地跑一遍确认脚本无误
- 提交代码(先提 PR,不要绕过)
- 合并后,CI/CD 自动执行迁移
关键原则:已合并到主分支的 V 脚本绝对不能再修改。如果要改表结构,新建一个 V 脚本,写 ALTER 语句。
6.2 版本号冲突
两人同时新建 V5,合并时必然冲突。解决方法:
- 合并代码时,后合并的人把版本号改成 V6
- 但要注意:如果 V5 脚本引用了 V4 的结构,V6 也得在 V4 之后执行,顺序要保持
- 团队规范:一人一次只占一个版本号,预留给其他同事
6.3 Checksum 不匹配
常见于以下场景:
- 有人直接改了已执行过的 SQL 脚本文件(不管空格还是注释都算)
- 数据库编码不一致导致 checksum 计算偏差
- 从 Windows 换到 Linux,换行符不同
解决方式(按推荐度排序):
- 最佳:改回去,新建一个 V 脚本来做变更
- 临时开发环境:执行
flyway repair重置 checksum(生产环境禁止) - 极端情况:手动更新
flyway_schema_history表的 checksum 字段(不建议,除非你清楚后果)
6.4 多模块项目
一个 SpringCloud 微服务项目,多个模块各有自己的数据库,怎么管理?
方案一:多数据源 + 多 Flyway 实例(推荐)
@BeanpublicFlywayorderFlyway(@Qualifier("orderDataSource")DataSourceds){returnFlyway.configure().dataSource(ds).locations("classpath:db/migration/order").table("flyway_schema_history_order")// 自定义历史表名,避免冲突.load();}方案二:一个 Flyway 管理多个 schema(不太推荐,耦合度高)
spring:flyway:schemas:order_db,device_db,payment_db6.5 和 Liquibase 的对比
| 维度 | Flyway | Liquibase |
|---|---|---|
| 迁移脚本格式 | 纯 SQL | XML / YAML / JSON / SQL |
| 学习成本 | 低(你会写 SQL 就会用) | 中(需要学 Liquibase DSL) |
| 灵活性 | 中等 | 高(支持条件逻辑、回滚、多环境) |
| 社区规模 | 大 | 大 |
| SpringBoot 集成 | 开箱即用 | 开箱即用 |
简单说:如果团队只会 SQL,选 Flyway;如果需要复杂的分支合并、多环境差异化配置,选 Liquibase。
对我们无人售货柜项目来说,Flyway 完全够用——我们不需要 rollback 到任意版本,只要保证"每次 SQL 变更都被可靠地执行一次"就够了。
6.6 无人售货柜项目中的 Flyway 实践
我们项目上线时的数据库版本演进:
V1 → 初始化:device 设备表、goods 商品表 V2 → 新增 firmware_version 固件版本字段 V3 → 创建 order 订单表(含支付状态、设备关联) V4 → 加索引:idx_device_code、idx_order_create_time V5 → 新增 refund 退款表 V6 → order 表加 refund_reason 字段 V7 → 创建 ai_recognition_result 视觉识别结果表 V8 → 加复合索引,优化识别查询性能 V9 → 新增 device_heartbeat 心跳表(MQTT 设备在线管理) V10 → goods 表加 temperature_range 温控配置字段 ...每次迭代,DBA(其实就我自己)写好迁移脚本,放进db/migration目录,代码一合,CI 流水线跑完自动部署,数据库和代码始终保持同步。再也没出现过"SQL 忘了执行"的事故。
七、总结
Flyway 不是什么高深的技术,但它解决的是软件工程里一个非常基础、非常容易翻车的问题:如何让数据库的变更和代码的变更保持同步。
核心要点:
- V 脚本:版本化迁移,有去无回,递增不可改
- R 脚本:可重复执行,适合视图和存储过程
- flyway_schema_history 表:Flyway 的记忆,别手动动它
- baseline-on-migrate:已有项目的救星
- checksum 不匹配就报错:这是保护机制,不是 bug
- 已合并的脚本不要改:永远向前,不回头
如果你正在维护一个 SpringBoot 项目,团队超过两个人,数据库还没用 Flyway——强烈建议现在就加上。几分钟的配置,换来的是未来无数次"SQL 脚本漏执行"事故的避免。
干技术的都知道:能被工具自动解决的问题,就不要靠人的记性去保证。Flyway 就是这样一个工具。