
1. 为什么我会盯上这个数据库版本管理难题先讲个真实场景。我之前参与的一个老项目数据库脚本全靠一个共享文件夹管理里面躺着几十个命名混乱的SQL文件update_20240110.sql、final_v3.sql、真的最后一次.sql——后面这个文件真的存在。每次上线团队里最资深的那个同事搬出他的数据库变更记录.xlsx对着线上库手动执行脚本少了哪个、重复执行了哪个全靠人力盯。终于有一次某张表的字段在测试环境加了线上没加生产事故凌晨三点拿备库数据拯救业务。那之后我就开始找工具目标很明确像管理代码版本一样管理数据库结构变更。Git管住了Java代码却管不住application的表结构变动这不合理。找了一圈锁定Flyway——一个开源的数据库迁移工具老牌、成熟、和Spring Boot生态配合极好基本属于Java后端项目里的标配。简单说Flyway做的是这样一件事把所有数据库结构变更脚本它叫Migration迁移脚本纳入版本化管理按顺序执行记录执行历史每个脚本只能成功执行一次。团队里任何人都能通过flyway migrate把本地库升级到最新结构不用靠人肉去数这个脚本我执行过没有。它不是什么银弹但能把那一大块靠微信群Excel记忆力维持的隐性流程变成一条可审计、可重放、可自动化的显性链路。这篇文章适合谁后端开发、DBA、运维、项目架构师尤其是数据库脚本管理仍停留在人肉执行阶段、或者已经开始感受到多人协作下数据库变更混乱的团队。下面我会从核心机制讲起把四种迁移类型、集成方式、坑点全部分享出来最后给出一套可以直接落地的团队规范。2. Flyway的迁移机制从Schema History表说起2.1 它的第一板斧第一个人建了张台账表理解Flyway最好的切入点是看flyway_schema_history这张表。当你在一个空数据库上第一次执行Flyway时它会自动创建一张历史记录表表名可用history参数自定义然后把每一次成功执行的迁移脚本记进去。这张表的关键字段就这么几个字段含义什么时候用installed_rank执行顺序号决定脚本执行先后version脚本版本号识别哪个版本description描述信息日志和排错用type迁移类型SQL还是Java版本化还是可重复checksum脚本内容校验和防止已执行脚本被篡改success是否成功失败记录保留便于回溯这张表是整个机制的基石。有了它Flyway每次启动时先读表对照文件系统里的迁移脚本判断哪些是新的、哪些已经执行过、哪些被改过。你可以把它类比成身份证管理系统数据库结构变更的历史全部有据可查没人能偷偷改历史。2.2 执行流程四条规则定生死Flyway执行迁移的逻辑不复杂但规则非常严格排序按版本号顺序执行1.0, 1.1, 2.0...版本号不是数字大小比较是分段比较语义1.10大于1.9。只执行未记录的如果脚本版本在历史表里不存在说明还没执行过此次执行。校验已执行的如果脚本版本存在但内容checksum和文件系统里的不一致直接报错Validate failed拒绝启动。事务性默认每个迁移脚本运行在一个事务里DDL和DML要么全成功要么全回滚。注意MySQL因为历史原因DDL会隐式提交这个后面单说。这套流程给我最深的印象是它把数据库变更这件事从经验活变成了流水线活。以前一个人拍脑袋决定脚本顺序现在由工具按规则强制执行以前脚本被人改了没记录现在checksum校验直接拦住你。2.3 脏数据库与baseline接手存量项目的关键文章开头提到的老项目就是典型的存量库——数据库已经存在一堆表了不可能把库删了重建。这时候直接用Flyway它会发现库里没有空表也没有flyway_schema_history表默认会拒绝执行或者根据配置决定行为。解法是baseline。简单说对着已有数据库执行flyway baseline它会记录一个baselineVersion比如1.0并把当前这个状态标记为基线后续迁移从1.1开始执行。相当于告诉Flyway已经存在的库结构我不管了从1.1开始新账旧账分开算。注意baseline 前要确保baselineOnMigratetrue或者手动执行 baseline 命令。生产环境的存量库建议手动确认后再 baseline不要盲目依赖自动配置。这一点对从0开始引入Flyway的团队来说是第一道坎处理不好会在第一步就劝退不少人。3. 三种迁移类型的边界与使用策略3.1 Versioned Migration一次性的版本化变更这是用得最多的一类文件名格式为V版本号__描述.sql比如V1.0__create_user_table.sql。这类脚本只执行一次执行过的版本永远不会再执行。适合放哪些内容CREATE TABLE/ALTER TABLE/ADD COLUMN等DDL结构变更数据初始化初始管理员账号、字典表基础数据需要变更后又回滚的复杂数据修复用SQL写不是塞进代码里我的经验是DDL是版本化迁移的主战场。不管什么环境表结构永远一致它解决的是测试环境改了、线上忘了改的致命问题。3.2 Repeatable Migration可重复执行的Ref脚本文件名格式R__description.sql中间没有版本号。这类脚本在每个flyway migrate都会重新执行只要checksum变了就重跑没变就跳过。适合放什么视图CREATE OR REPLACE VIEW存储过程、函数定义需要保持最新定义的数据库对象物化视图刷新逻辑、通用函数核心逻辑和Versioned不一样它没有顺序约束每次启动都会看一遍。所以视图或函数里不能写绝对依赖某个严格顺序的内容否则你会在某个环境跑挂。3.3 Java-based Migration代码迁移的适用场景不是所有变更都能用SQL完成。比如需要读取外部系统数据、调用远程API补数据、做一些复杂的数据清洗SQL写起来痛苦或者完全没有SQL表达那就写Java迁移类。实现方式是实现BaseJavaMigration接口重写migrate方法。文件命名也有规范V1_1__MigrateUserData.java和SQL迁移的版本号对应。但Java迁移有一个大坑已经部署过、执行过的Java迁移类不能改逻辑。因为checksum对这个类是计算过并记录的你改了代码再启动校验直接失败报Validate failed。所以Java迁移写起来要格外谨慎上线前反复测试上线后绝不轻动。3.4 四种模式的实操选型表场景迁移类型文件名示例注意事项建表、加字段、改约束VV1.1__add_order_no_uk.sql一版本只做一件事便于定位问题视图/函数/存储过程RR__v_user_detail.sql脚本里必须用CREATE OR REPLACE复杂逻辑数据修复JavaV1_2__BackfillOrderUserId.java执行后严禁修改类逻辑存量库初次引入baseline VV1.0__baseline_mark.sql先手动确认表结构现状4. Spring Boot项目里的集成实操4.1 依赖引入与最小配置Flyway对Spring Boot的支持非常完善项目里只需要两步就能跑起来。第一步pom.xml加依赖dependency groupIdorg.flywaydb/groupId artifactIdflyway-core/artifactId /dependency如果用的是Spring Boot 2.x和MySQL需要确认flyway-core版本和MySQL驱动版本匹配。Spring Boot 2.7自带Flyway 8.x更高版本的Flyway 9需要额外关注兼容性。第二步application.yml里加配置spring: flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true如果项目是Spring Boot 3.x MySQLFlyway在9.x/10.x对MySQL的模块进行了拆分需要额外引入dependency groupIdorg.flywaydb/groupId artifactIdflyway-mysql/artifactId /dependency4.2 配置项的最佳实践上面配置里那几个开关每一个都值得认真对待。baseline-on-migrate: true适合开发环境的快速起步但线上环境建议刚开始引入时手动执行一次flyway baseline确认无误后再交给应用自动管理防止误判。validate-on-migrate: true是我强烈建议打开的。它能在应用启动时把Classpath上的迁移脚本和数据库历史表的checksum做一次比对。如果发现某个已执行的V脚本内容被人改了启动直接抛异常。这正是防止改了历史脚本这种隐患的关键防线。locations指定脚本目录。默认是classpath:db/migration如果你的脚本在别的位置比如db/migration/{vendor}也可以指定spring: flyway: locations: classpath:db/migration/{vendor}Flyway会按照数据库类型自动选择对应的子目录比如mysql子目录这对多数据库支持的项目很友好。4.3 多数据源场景每个数据源都要管如果项目里配了多个数据源比如主库、从库只读、历史库Flyway只默认管理主数据源其他数据源需要单独声明。Spring Boot 2.x下的多数据源接入Flyway不复杂核心就是给每个DataSource配一个独立的Flyway实例。常见做法是把Flyway配置类按数据源拆分Configuration public class SecondaryFlywayConfig { Bean public Flyway secondaryFlyway(Qualifier(secondaryDataSource) DataSource dataSource) { Flyway flyway Flyway.configure() .dataSource(dataSource) .locations(classpath:db/migration-secondary) .baselineOnMigrate(true) .load(); flyway.migrate(); return flyway; } }多数据源的项目里脚本目录就一定要分清楚。主库和从库的结构如果完全不同把脚本混在一起跑要么重复执行要么互相干扰。4.4 启动流程里发生了什么Spring Boot应用启动后Flyway的执行时序大致是数据源初始化完成Flyway获取数据库连接检查并创建flyway_schema_history表比对历史记录与本地脚本执行新迁移脚本事务提交记录历史这个过程对应用启动时间有一定影响脚本多了、复杂了启动会明显变慢。开发机上尤其明显。建议首次接入时控制脚本数量每个脚本尽量精简别把几百行SQL塞进一个迁移文件里。5. 多环境与团队协作中的实战细节5.1 dev、test、prod三环境的管理思路多环境管理是Flyway用得最多的痛点场景。我的思路是一套脚本扩展到所有环境用环境变量控制不敏感参数靠人为而非工具来隔绝环境差异。具体做法所有迁移脚本只有一份放在classpath:db/migration下dev、test、prod都跑同一套。有些环境参数需要不同比如初始化的管理员手机号、邮件服务器地址这种可以抽到application-{profile}.yml里脚本里通过SQL变量或者应用层注入来区分。Flyway原生不擅长同一条SQL在不同环境下执行不同数据这种情况下宁可把数据迁移做成两个V脚本一个V__init_dev_data.sql一个V__init_prod_data.sql用配置指定locations来做分流也别在脚本里写环境判断的SQL。开发环境每天可以删库重建但prod库不能动。所以clean命令清空库只允许在dev/test环境配置线上环境永远不要配置clean勾选除非你想体验删库跑路的刺激。5.2 迁移脚本的命名的团队纪律Flyway本身对文件名规范很宽容只要V数字__描述.sql就能跑但团队协作里确实有规范和团队觉得有规范是两回事。我把我们团队的脚本命名规范列出来你直接抄V1.0__init_db.sql -- 基础表结构 V1.1__add_order_table.sql -- 新功能表 V1.2__add_order_index.sql -- 索引变更 V1.3__add_user_telegram_column.sql -- 字段变更 R__v_user_overview.sql -- 视图定义这几点比较关键版本号要么固定两位小数1.1、1.2要么递增整数1、2不要混着用。V1.1和V1.1.1在Flyway的版本排序里是不同的层级混着用容易产生预期之外的执行顺序。描述部分用下划线分隔语义尽量言简意赅会用diff的同事一眼能看出脚本的目的。大的结构重构拆成多个迁移每一版对应一个功能模块。不要出现一个脚本里既改了订单表又改了用户表再加了缓存表排查问题的时候会非常痛苦。还有一条纪律已经合并到master并上过生产的迁移脚本任何人不得以任何理由修改其内容。有变更需求就新建一个V脚本去修改。这条纪律重要到什么程度——如果有同事改了旧脚本并成功启动配置了validate-on-migrate: true的团队会在下一个版本里被直接拦下来报错信息明明白白写着Migration checksum mismatch for version 1.2。5.3 验证方案我在本地怎么确保脚本没问题脚本写完之后不要直接合并且等CI执行本地先跑一遍。我一般这样验证准备一个空的本地MySQL实例在项目根目录执行mvn flyway:migrate或者让Spring Boot启动自动执行检查flyway_schema_history表确认所有脚本执行成功、success列都是1检查目标表结构是否符合预期表注释、字段注释、索引都逐一确认如果有R__脚本改一下脚本内容再执行一次确认它会重新执行这套流程跑下来脚本能不能上生产基本心里有数了。如果本地都不跑直接推代码测试环境挂了迟早变成你的口头禅。6. 我踩过的坑与解决方案比官方文档多走三步6.1 MySQL的DDL隐式提交陷阱这是MySQL用户最容易踩的坑而且踩了以后很难察觉。Flyway默认把每个版本化迁移脚本包在一个事务里执行说好的失败回滚。但MySQL对CREATE TABLE、ALTER TABLE、DROP TABLE等DDL语句会隐式提交——也就是说脚本里第一条ALTER TABLE执行成功后事务就已经提交了后续语句再失败前面成功的部分不会回滚。举个例子ALTER TABLE user ADD COLUMN age INT; ALTER TABLE user ADD COLUMN email VARCHAR(255);如果第二句因为字段名冲突执行失败整个迁移标记为失败flyway_schema_history里会有一条success0的记录。但age字段已经加上了不会回滚。下次你再执行flyway migrate会因为历史表里存在失败记录而直接拒绝或要求先修复。我的处理方式给MySQL环境下的团队立了三条规矩每个迁移脚本尽量只包含一条DDL。加多个字段就拆成多个文件别图省事任何DML和DDL混写的脚本先执行DDL再执行DML降低回滚损失如果脚本失败绝对不要在本地把失败的迁移记录删掉再重跑正确做法是写一个新的修复脚本-- V1.3.1__fix_user_email_column.sql ALTER TABLE user ADD COLUMN email VARCHAR(255) COMMENT 邮箱;记录留在历史表里不可怕可怕的是你以为跑过了实际字段没加上。6.2 视图/函数里的SQL检查依赖旧表结构R__脚本最容易出问题的地方在于开发者在开发分支上写了一个视图引用了某个新加的字段然后忘了这个视图在旧环境上执行的时候那个字段可能还不存在。因为R__脚本每次启动都会执行只要checksum变了一旦视图引用了不存在的字段migrate直接报错。有个真实教训我们给订单详情页加了个payment_method字段同时改了R__v_order_detail.sql引用它。本地测试环境因为迁移顺序没问题一切正常。但生产环境执行的时候V1.3__add_payment_method.sql和R__v_order_detail.sql是两个独立脚本同一批执行里顺序反了视图先跑字段还没加上直接报错。解决思路是两种把视图创建脚本从R__改成V__明确依赖顺序。但这样一来每次改动视图都要新增一个版本历史记录会越来越长R__脚本内部写成幂等且健壮的形式用IF条件判断字段是否存在再创建视图CREATE OR REPLACE VIEW v_order_detail AS SELECT * FROM order WHERE paid_at IS NOT NULL;虽然没有原生条件语法但整体SQL写法尽量兼容性强不依赖特定字段的存在性。我的建议是重要视图用版本化迁移管理低频变动的通用视图用R__脚本。前者保证顺序可控后者保证定义始终最新各取所长。6.3 Java迁移类不能被维护的代码Java迁移类是所有类型里最需要克制的一类。我见过最惨痛的案例同事写了个V2_5__MigrateOrderData.java做历史订单数据回填上线后发现业务规则理解错了数据回填错了。大家的第一反应都是改Java代码修数据重新跑一次。然后启动直接报Validate failed因为已执行的Java迁移的checksum变了。Java迁移类的正确处理方式组合上线前用各种边界数据测试确保逻辑正确一旦执行过绝不修改原类。写一个V2_6__FixMigrateOrderData.java把新逻辑放进去代码里尽量用原生SQL操作数据库别引入复杂的服务依赖。因为迁移类执行时应用上下文可能还没完全初始化强依赖Spring容器的迁移类迟早出问题6.4 跨环境的checksum校验问题最后说一个容易被忽略的细节不同Flyway版本对同一个SQL文件的checksum计算方式可能不同。从Flyway 8升级到Flyway 9可能遇到已执行脚本的checksum全部不匹配启动直接失败。这不是脚本写错了是工具自身规则变了。处理办法也比较直接升级Flyway前先在测试环境全量执行一遍flyway migrate确认正常准备一个V__repair_history.sql脚本或者使用flyway repair命令修复历史表的checksum字段团队内部统一Flyway版本别出现你本地是8.xCI是9.x这种分叉Flyway的repair命令本质上就是把历史表里的checksum重新计算并更新它不会执行任何脚本只是恢复校验基线。真出问题的时候先冷静判断是脚本被改了还是版本升级导致的再决定用repair还是修复脚本。7. 落地上线两周后的效果与收尾心得我们的老项目切到Flyway到现在快两年了。最直观的变化是线上出问题的频率显著下降。之前那种线上表少了个字段、测试环境和线上结构对不上的故障再也没出现过。以前每次发版都要约着DBA排期执行脚本现在应用启动时自动迁移流程干净利落。如果团队还没有引入数据库迁移工具我的建议是从小范围试点开始不要一上来就把所有存量脚本整理成几百个V文件。先接一个模块跑通流程让团队成员体验一下脚本自动执行、历史可查的爽感再逐步扩大覆盖范围。我个人在操作中的体会是Flyway真正解决的不只是执行SQL这个动作而是把数据库变更的所有权从少数人的脑子里转移到了人人可见的代码仓库里。它让谁的脚本、什么时候执行、执行了什么、有没有失败全部变成了一目了然的事实而不是靠记忆和信任维持的默契。最后送一条经验如果团队里有人问这SQL脚本是不是执行过了说明你们的数据库迁移管理还没到位。把Flyway用起来之后这个问题会彻底消失——打开flyway_schema_history表每一笔变更都清清楚楚躺在那里。