Flyway实战:像管理代码一样管理数据库版本与迁移 1. 从一次“数据库不一致”事故说起前阵子公司上线一个新版本开发环境、测试环境跑得好好的结果一上生产就出幺蛾子前端页面报“字段不存在”后端日志一片红色。排查了半天发现是生产库少了 migration 脚本里新增的一列。为什么因为DBA拿到SQL脚本后不确定哪些已经执行过就挑着执行了几个结果漏了最关键的一个。类似这种“数据库脚本管理靠口口相传”的坑我相信干过几年后端的人都遇到过。代码有Git管着任何人改了代码都有迹可循可数据库结构呢往往就是一堆 SQL 文件扔在目录里谁改过、谁执行过、执行到哪一步全靠问。项目一跨组、一换人数据库结构立马就成了黑匣子。这也是我这一年把 Flyway 全面铺到项目里的直接原因——让数据库结构的变更像代码一样有版本、可追溯、能自动执行。Flyway 是我目前用过最好上手的数据库版本自动化管理工具没有之一。它解决的核心问题很简单每次代码发版时数据库的表结构、基础数据需要做什么变更由工具自动检查并执行不用再人肉整理、反复确认“这个脚本跑没跑过”。这篇文章不跟你扯大而全的官方文档我就按自己从接手遗留老项目、到搭新项目、再到处理生产事故的真实经历把 Flyway 的核心理念、关键配置、实操流程和踩坑记录一次性讲清楚。无论你是刚入行的后端开发还是正在为多环境数据库一致性头疼的团队负责人应该都能在里面找到直接能用的干货。2. 为什么我们需要数据库版本管理2.1 没有版本管理时数据库是怎么失控的先还原一下最常见的“裸奔”场景。项目初期一个人开发库表随便改本地库删了重建也就几分钟的事。没人在意数据库结构变更的记录。然后团队变大了分工了张三负责用户模块李四负责订单模块两人同时改各自的表。约定是“谁改了库就发个 SQL 到群里大家自己执行一下”。第一天没事第二天有人忘记执行第三天有人重复执行报错第四天发现张三的本地库结构和李四的完全不一样一顿操作后代码合并了库结构却合并不了。这个场景是不是听着很熟悉更要命的是环境之间的差异。开发环境库表结构早就跑乱了测试环境靠 DBA 手工刷脚本维持生产环境更是“不能随便动”。每次发版最紧张的不是代码合并而是“这次上线到底要执行哪些SQL”。漏一个脚本上线后就是事故多执行一次轻则报表重复数据重则直接跑崩。所谓“发版两小时数据库折腾半小时”说的就是这种状态。本质上代码有 Git 管但数据库结构一直是“游离”在版本控制之外的。这就是问题的根源。2.2 Flyway 的核心设计思路Flyway 做的事情说白了就是把“数据库结构变更”当成代码来管理。它的工作方式非常直观给每一次数据库变更定义一个带版本号的脚本文件比如V1.0__create_user_table.sql把这些脚本放进项目里统一管理。Flyway 会维护一张专门的记录表默认叫flyway_schema_history里面存着哪些版本执行过、执行时间、执行结果的 checksum 校验值。每次执行时Flyway 自动对比当前数据库的版本记录和项目里的脚本列表找到还没有执行过的脚本按版本号从小到大依次执行然后更新记录表。我当初看文档时脑子里冒出来的类比就是Flyway 就是数据库世界的 Git。Git 用 commit 记录代码的每次变更Flyway 用版本脚本记录数据库的每次变更。Git 有分支、有合并Flyway 有基线、有按序执行。一旦理解了这一层Flyway 的所有规则就都顺理成章了。这个设计的好处至少有三点。第一自动化执行。不需要人肉记“这个脚本跑没跑”Flyway 通过记录表自动判断。新环境部署时一次migrate命令库表结构直接从空库到最新版本。第二校验一致性。Flyway 会对已执行脚本做 checksum 校验。如果你把已经执行过的V1.0__xxx.sql改了内容哪怕只改了一个空格Flyway 都会报错阻止你继续执行。这就是在逼你规范化地处理变更已发生的版本要么通过新增脚本去修要么走repair流程不能偷偷改历史。第三统一多环境。开发、测试、生产只要都接上 Flyway执行同样的脚本序列库表结构理论上就是一致的。再也不用担心测试环境漏了一个字段、生产环境多了一张表这种诡异差异了。2.3 什么时候需要引入什么时候先别急当然我也不是建议所有项目无脑上 Flyway。我自己的判断标准是这样的需要引入团队两人以上、有独立测试环境、发版频率稳定、表结构会持续变更的项目。这类项目对数据库变更的追溯性和一致性要求高把结构变更纳入自动化管理是刚需。可以先不引入演示项目、原型项目、一次性脚本工具。这种场景下引入反而会增加成本比如每次改表都要写脚本、维护版本记录对快速验证想法的项目来说属于拖后腿。我自己的一个体会是Flyway 越早引入越省心。项目一旦跑了一年半载库表几十张脚本遍地都是这时候再引入 Flyway 就需要处理“存量基线”问题麻烦一些但也不是不行后面我会专门讲这块。3. Flyway 核心概念与关键配置解析3.1 脚本命名规范规则的基石Flyway 对迁移脚本的命名有一套严格的规范直接决定工具能不能正确识别和排序。基本格式是VERSION__DESCRIPTION.sqlVERSION 就是版本号__后面是描述信息。版本号可以是1、1.1、2.0.1这种带小数点的形式描述里面用下划线代替空格或者用-也可以看团队习惯。举个例子V1__create_user_table.sql V1_1__add_user_age.sql V2_0__create_order_table.sql版本号主要用来决定执行顺序Flyway 会比较版本号从低到高依次执行。有一个细节值得注意V1.1是排在V1后面的并不是说V1.1是依赖在V1之下的Flyway 执行所有脚本时不区分“大版本”和“小版本”只看数字大小。很多人第一次用的时候容易犯一个低级错误把文件名写成了V1.0_create_user_table.sql注意版本和描述之间必须是两个下划线不是连字符也不是一个下划线。单下划线会被当成版本号的一部分导致识别异常。Flyway 的社区版支持两种脚本类型版本化迁移Versioned Migration和可重复迁移Repeatable Migration。前者用V开头后者用R开头比如R__init_metadata.sql。可重复迁移的特点是每次执行 migrate 时都会重新执行适合放视图、存储过程这类“需要幂等更新”的对象。普通表结构变更一律用版本化迁移。3.2 核心命令migrate、info、validate、repair 的用途边界Flyway 提供了几个命令我平时用到的核心就这几个每个对应的场景我分别说下migrate核心主力命令。构建工具或应用启动时会自动调用它把未执行的版本脚本跑一遍并记录到历史表。日常开发基本只需要它。info查看当前数据库的版本状态打印所有已应用和待应用的迁移脚本。排查问题时我会先用它确认当前库到底跑到了哪个版本再对比项目里的脚本列表差值一目了然。validate校验已应用脚本与项目文件是否一致。Flyway 记录表里存了 checksumvalidate 会对脚本重新计算校验值做比对。如果有人改过历史脚本execute 时 Flyway 也会自动检查然后抛异常。所以平时直接跑 migrate 就带了这层校验validate 更像是摸底用的排查工具。repair修复 flyway_schema_history 的记录问题。比如某个脚本执行到一半失败了残留记录或者你误改了历史脚本想让 Flyway 重新计算校验值。但注意repair 只是修记录不是修数据。脚本里写的 SQL 实际发生了什么还得人肉去看。还有一个好用的功能是baseline它负责“从存量数据库开始接管”。如果你的项目已经运行了大半年库里一堆表现在才引入 Flyway可以用baseline-on-migrate: trueFlyway 在第一次执行时会自动以当前数据库状态为基线默认基线版本是 1把它记录到历史表里然后从版本 1 之后的脚本开始执行。这个做法我后面会展开讲因为坑最多。3.3 配置参数解读真正影响行为的关键选项Flyway 的配置参数有不少但日常开发真正需要关注的我认为主要是这几个。locations定义扫描脚本的路径默认是classpath:db/migration。多模块项目可以配置多个路径比如spring: flyway: locations: - classpath:db/migration/common - classpath:db/migration/biz需要注意多个 locations 之间的脚本版本号必须是全局递增的不能出现两个目录里都有V2__xxx.sql不然会报重复版本。我的建议是除非项目特别大需要按业务拆分否则统一放一个目录避免管理成本。baseline-on-migrate 与 baseline-version刚才提过baseline-on-migrate: true表示第一次 migrate 时自动对已有数据库做基线baseline-version指定基线版本号默认是 1。这组参数主要给存量项目用新项目不需要开。out-of-order允许乱序执行版本脚本。默认是false。什么意思呢举个例子你有一个V1__a.sql和V3__c.sql已经执行过了这时候补了一个V2__b.sql进去默认情况下 Flyway 会报错因为它的执行逻辑是“从当前版本往后找未执行的且版本号必须大于当前最大已执行版本”。开了 out-of-order 后V2会被执行记录到历史表里。这个功能在多分支并行开发、合并时很实用但也有风险——如果V2脚本里依赖了V3已经执行过的某个结构变更就可能在补跑时出问题。我的实际建议是如果你们团队没有严格的分支管理可以先开着 out-of-order但脚本内部要约定好尽量保持幂等、尽量用IF NOT EXISTS之类的保护性语句把乱序执行的风险降到最低。如果团队能管好分支就保持默认false更安全。table指定 Flyway 历史记录表的表名默认是flyway_schema_history。一般不用改。但如果你在一个数据库里同时管理多套业务表为了防止表名冲突可以分别指定不同的历史表比如spring: flyway: table: flyway_order_historyclean 命令的警示Flyway 有一个 clean 命令会把库里的所有对象全部 drop 掉然后重建。听着吓人也确实凶险。我强烈建议不管什么环境永远不要把 clean 和 migrate 配在同一个流程里更不要在生产环境的脚本里写 clean。这属于上线事故的经典来源之一。开发环境偶尔用用方便重置其他环境别碰。3.4 历史表怎么读一个实际例子Flyway 每次执行完 migrate都会往flyway_schema_history表插入一条记录。表结构大致是字段说明installed_rank安装顺序自增version版本号description描述type迁移类型SQL/JAVAscript脚本文件名checksum脚本内容校验值installed_by执行人数据库账号installed_on执行时间execution_time耗时毫秒success是否成功0/1日常排查问题时我习惯先查这张表按 installed_rank 倒序看最近执行的版本和状态。success 为 0 的记录说明有脚本失败过需要重点处理。这个表也算是团队协作的一个“数据库变迁记录总账本”比群里翻聊天记录靠谱太多了。4. Spring Boot 集成 Flyway 的实操流程4.1 最简接入一个依赖加一段配置如果你用的是 Spring BootFlyway 的接入简直可以用“无感”来形容。引入依赖dependency groupIdorg.flywaydb/groupId artifactIdflyway-core/artifactId version9.22.3/version /dependency如果你的 Spring Boot 版本比较新比如 2.7 或 3.xFlyway 7 以上版本需要配合对应的数据库驱动比如 MySQL 8.x 就要注意 flyway 对 MySQL 的支持版本要求。我遇到过 flyway-core 8.x 配 MySQL 8.2 起不来的问题后来升级到 9.x 就正常了。然后在application.yml里配上数据源Spring Boot 会自动用主数据源再指定一下 Flyway 的配置spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: false encoding: UTF-8然后在src/main/resources/db/migration目录下放脚本启动项目Flyway 就会自动执行。就这么简单。我都忘了第一次跑通时的场景了反正就两个词编译、启动、自动执行没了。不过要提醒一句如果项目里原来没有配置spring.flyway任何参数只要 classpath 里有 Flyway 依赖Spring Boot 会默认开启它并默认从classpath:db/migration扫描脚本。所以如果你不打算用 Flyway引入依赖后请记得显式设spring.flyway.enabled: false不然启动就会报错。别问我为什么知道问了就是泪。4.2 从空库到最新版本第一次执行的完整过程假设我们是一个完全从零开始的新项目。第一次启动时数据库是空的。Flyway 会经历这样几个步骤Flyway 检查目标数据库配置的数据源是否为空库。如果为空库不做 baseline直接按顺序执行所有版本化脚本。创建flyway_schema_history表并把每次成功执行的脚本信息插入进去。如果数据库里已经有一些表了且没有开 baselineFlyway 默认会报错“Found non-empty schema(s) but no schema history table”。这时候要么开 baseline要么先清理掉非空表。实际中第一步最常出问题的就是第 4 步。比如你把 Flyway 加到已有的一个老项目里库里已经一大堆表了启动时就报这个错。解决办法就是上面说的配置baseline-on-migrate: true让 Flyway 以当前库状态为背景建立历史表之后的管理就完全是常规 Flow 了。我当时第一次用 Spring Boot 接入时连数据库都忘了建启动报“Unknown database”这种低级错误后来建好库再重启一切顺利。4.3 脚本内容怎么写一次迁移一个逻辑变更关于迁移脚本我最想强调的一点是一个脚本只做一件事。所谓“一件事”是指一个逻辑变更。比如“用户表加一个年龄字段”就是一个独立变更这是一个脚本。不要在一个脚本里既建表、又改数据、又建索引。这样做的原因主要有两个可读性好。每个脚本的描述字段能直接说明意图review 的时候不用打开脚本才知道干了什么。失败后好恢复。Flyway 的单个脚本默认在同一个事务里执行MySQL 的 DDL 会自动提交这一点先按下不表后面说。如果脚本太大一次报错很难定位到底跑到了哪一句。举个例子一个标准的建表脚本-- V1__create_user_table.sql CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(64) NOT NULL COMMENT 用户名, email VARCHAR(128) DEFAULT NULL COMMENT 邮箱, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;再来一个加字段的版本脚本-- V2__add_user_age.sql ALTER TABLE user ADD COLUMN age INT DEFAULT NULL COMMENT 年龄 AFTER email;如果业务需要初始化基础数据比如插入一些配置项也可以用版本脚本。但要注意如果脚本里既有 DDL 又有 DML请确保在同一个事务里执行是安全的。MySQL 的 DDL 是隐式提交的一旦脚本中 DDL 执行成功后 DML 失败事务回滚只能回滚 DML 部分DDL 的变更就留下了。这个问题不是 Flyway 能解决的脚本设计阶段就得想好。4.4 执行结果怎么验证不只是日志Flyway 执行成功后控制台会打印迁移摘要Spring Boot 集成后日志也会显示类似INFO --- [main] o.f.c.i.database.DatabaseFactory : Database: jdbc:mysql://localhost:3306/demo_db (MySQL 8.0) INFO --- [main] o.f.core.internal.command.DbMigrate : Current version of schema demo_db: 1 INFO --- [main] o.f.core.internal.command.DbMigrate : Migrating schema demo_db to version 2 - add user age INFO --- [main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema demo_db但就算日志显示成功了我也建议顺手做两件事验证一下第一查一下历史表确认记录没问题SELECT installed_rank, version, description, type, script, success FROM flyway_schema_history ORDER BY installed_rank;第二在数据库客户端里看一下表结构是否真的变更了。日志有时候会因为各种原因盖过一些信息真实的数据结构才是最终答案。5. 多环境、存量库与团队协作三种典型场景5.1 存量老项目引入 Flywaybaseline 的正确姿势这是我觉得最值得展开的实操场景因为接手的项目大部分都是已经跑了好几个月甚至好几年的。假设你要把一个没听过的老平台接进 Flyway第一步是盘清楚当前库里所有对象表、视图、函数、存储过程。这时候可以用几个信息 SQL 把库里的对象列出来比如SHOW TABLES;然后配置里加上spring: flyway: baseline-on-migrate: true baseline-version: 1启动后Flyway 会在没有历史表的情况下把这些非空对象作为“基线版本 0”或“基线版本 1”记录到历史表里。注意Flyway 的默认基线版本是 1如果你项目里后续第一个迁移脚本想从V2开始就把baseline-version设为 1第一个脚本用V2开头。如果第一个新脚本想用V1就得把 baseline-version 设为 0不然会报版本冲突已存在基线 1又来一个 V1。这是最容易踩坑的地方。我接手过一个模块库里对象不算多DBA 手动执行过十几个脚本版本和脚本文件完全没对齐。我当时的做法是把历史表清掉反正还不存在。baseline-version 设置成当时 DBA 手动执行到的最高版本号比如3。新增脚本从V4开始。但后来发现一个隐患baseline 之后Flyway 只知道“当前数据库的状态对应版本 3”但项目里V1到V3的脚本还在 locations 里。Flyway 默认不会再执行它们因为基线版本之后才是待执行的但也要求这些脚本的版本号不得与基线重合。如果 locations 里放了一个V1__xxx.sql而基线版本是 1Flyway 会认为你已经执行过版本 1 了不会重复执行一般不会报错。可如果 locations 里是V2__yyy.sql基线版本又恰好是 2这就有风险。所以存量库引入的稳妥做法是把历史脚本从 locations 目录移走只保留基线版本之后的脚本。避免 Flyway 对历史文件反复扫描校验也避免不小心重复执行。5.2 多环境同步一致性的来源就是按序执行开发、测试、预发、生产我目前维护的项目是四个环境。每个环境都配了各自的数据库连接但 Flyway 的 locations 指向的脚本是同一份代码。也就是说只要 git 分支一致四个环境执行 Flyway migrate 拿到的脚本序列就是完全一样的。效果也是立竿见影开发环境跑乱了直接跑 clean migrate 重建仅限开发库。测试环境发版DBA 不再需要问“这次上线有哪些SQL”直接让应用启动时自动 migrate 一次。生产环境上线前我在预发环境跑一遍 migrate再对比历史表差集基本就能预估生产环境的执行结果。有一个细节要注意多环境共享一套迁移脚本时不要在脚本里写“环境相关”的配置。比如某条 SQL 只在开发环境执行不要在脚本里写WHERE env dev应该用独立的数据初始化脚本或者配合配置中心做环境隔离。Flyway 脚本一旦执行过就是历史和事实不能因为某个环境的不同而出现差异。5.3 团队协作规范让每个人提交脚本像提交代码一样自然Flyway 用起来很简单但要让团队里每个人都用得好需要立几条规矩。我目前团队内部执行的约定是所有数据库结构的变更一律通过 Flyway 脚本提交不通过数据库客户端手工改。脚本命名格式统一V版本__描述.sql版本号小组内协调尽量递增。如果出现两位同事同时改了脚本合并代码时以版本号更大的一方为准小的一方改版本重发。一个脚本就是一次 review 单元。代码评审时脚本和代码一起提交一起看DBA 也参与评审脚本内容。严禁修改已执行过的脚本。哪怕只是注释里的错别字也通过新增脚本来修。这套规矩跑了大半年最明显的变化是没人再在群里喊“我改了表你们手动执行一下这个 SQL 了”。因为大家知道只要代码合并了Flyway 会自动执行所以不用通知。6. 常见问题与排查技巧实录6.1 checksum 校验失败拒绝执行这是刚用 Flyway 时最容易踩的坑。某个同事改了历史脚本V1__xxx.sql哪怕只改了注释或者格式化了一下Flyway 在下次 migrate 时就会报错Migration checksum mismatch for migration version 1含义历史表里记录的 checksum 和你当前文件内容的 checksum 不一致。Flyway 的保护机制生效了。处理方式如果确实需要改历史脚本分两种情况。如果这个脚本还没在任何环境执行过可以改然后跑flyway repair重新计算校验值只能修当前库的校验记录其他环境还没执行自然不受影响。如果已经执行过了尤其是生产环境已经跑过了就别改脚本了通过新增一个版本脚本来覆盖变更。记住一个原则历史脚本是不可变的所有变更通过新增脚本推进。这跟代码里 commit 的历史不能随便 rebase 一个道理。6.2 脚本执行失败历史表残留 success0 记录Flyway 执行脚本时如果发生 SQL 错误默认会停止并在历史表里记录一条success0的记录。下一次 migrate 时Flyway 发现历史表中有失败记录会拒绝继续执行直到你解决完问题。常见的解决步骤先看日志定位失败 SQL。手动修复数据库里的实际问题比如 DDL 半执行完缺一个字段手动补上。处理历史表记录。如果是 MySQL 这类支持手动操作的场景可以DELETE FROM flyway_schema_history WHERE success 0;或者用修复命令flyway repair区别在于repair 会把这行失败记录标记一下不是简单删除delete 是直接清掉。我更推荐先人工判断失败脚本执行到哪一步再决定是补跑还是回滚。比如失败脚本执行到第三步时炸了前两步的 DDL 已经生效那你补跑时就得写一个“接着第三步开始”的脚本或者先手动把前两步回滚掉再重新执行原脚本。这个场景我建议在开发环境多练习几遍把流程摸透生产遇到时才不会手忙脚乱。我第一回遇到失败时慌了几分钟后来发现只要思路清晰其实就是“修数据、清失败记录、重跑”三部曲。6.3 脚本执行一半DDL 已提交MySQL 的事务局限MySQL 有个老毛病DDL 语句执行时会隐式提交事务所以 Flyway 的“单脚本单事务”保护在 MySQL 上并不完全可靠。如果你在一个脚本里先执行ALTER TABLE成功后面某条INSERT失败事务回滚时ALTER已经生效了不能回滚。针对这个问题我的处理策略是把 DDL 和 DML 分开放在不同脚本里。比如V3__add_column.sql只做结构变更V4__init_data_for_new_column.sql只做数据补充。这样即使 DML 失败结构和数据的状态也比较容易梳理。关键数据变更在脚本里加上“幂等”设计。比如插入前先判断是否已存在INSERT INTO config (key, value) SELECT order_timeout, 3600 WHERE NOT EXISTS ( SELECT 1 FROM config WHERE key order_timeout );这个问题本身是 Flyway 挡不住的事但了解之后脚本设计就会自然往“小而独立”的方向走。6.4 多分支开发版本号冲突该如何排解并行开发时常用 out-of-order 功能。我有一次给两个分支提交了V2__a.sql和V2__b.sql先合了 a后合了 b结果 b 分支直接报版本冲突。排查下来发现两个分支用了相同的版本号。处理方式是在合并时调整脚本版本号。b 的脚本改成V3__b.sql重新计算 checksum再执行迁移。如果你同时需要保留两个脚本的执行顺序就手动按合并顺序调整版本号即可。这个场景如果频繁出现我建议在团队里做一个版本号登记表或者干脆在 git 里约定一个version-map.md每个人开工前先看下当前最大版本号避免撞车。另外权限足够的话也可以让 merge 的负责人顺手统一调整版本号。6.5 常用问题速查表为了方便收藏我把常见问题整理成表格贴在这里问题现象可能原因处理方式Found non-empty schema(s) but no schema history table接入时库里已有对象打开 baseline-on-migrate设置正确 baseline-versionMigration checksum mismatch有人改过历史脚本生产环境别改历史脚本未执行的可执行 repairDetected failed migration to version X历史表有 success0 记录人工修复数据后删除失败记录或执行 repairDuplicate migration version 2两个脚本版本号相同调整其中一个版本号重新执行Flyway 启动报错 Unable to connect数据库驱动版本不匹配升级 flyway-core 或数据库驱动版本执行成功但表里没字段DDL 隐式提交DML 失败前已生效手动补字段或拆分 DDL/DML 脚本生产环境跑到了“多余”的版本手工执行脚本破坏了记录表结合 flyway_schema_history 对比必要时用 info 辅助排查这些坑我自己起码踩过一半。技术文档里不会把这些细节写得很透都是实际干活才碰得上的。7. 一点补充脚本文件的组织与代码评审7.1 目录结构怎么组织更清晰单模块项目最简单一个db/migration目录足够。稍微大一点的多模块项目我习惯给每个业务模块单独一个目录然后通过locations配置多个路径。但要注意多个目录之间的版本号必须全局唯一且不能跳号混乱。一个我觉得比较好用的目录组织方式src/main/resources/db/ └── migration/ ├── common/ # 全局基础表 ├── user/ # 用户模块相关 ├── order/ # 订单模块相关 └── data/ # 基础数据初始化脚本不过说实话对大部分中小项目一个目录就够。多个目录只是心理上觉得清晰实际执行规则完全一样反而增加“版本号撞车”的概率。我的建议是先单目录团队成熟了再按需拆分。7.2 脚本 review 要点我作为 reviewer 会看什么代码评审时我会重点看脚本几样东西版本号和文件名是否规范。不要有拼写错误、双下划线缺失。脚本内容是否只干一件事。DDL 和 DML 是否混写。SQL 是否有明确的字符集、引擎定义。MySQL 建表时如果没写CHARSETutf8mb4后续运维会遇到各种字符集问题。是否缺少必要的索引和约束。业务字段里如果明显有查询场景走一遍索引规划。幂等性设计。尤其是基础数据初始化脚本有没有WHERE NOT EXISTS或INSERT IGNORE之类的保护。可回滚性。脚本出问题后怎么恢复需不需要单独写一个“补偿脚本”。这个在 Flyway 本身不提供回滚机制的情况下尤其重要——方案一般是写反向的变更脚本比如ALTER TABLE DROP COLUMN这种但不建议放进常规的升级脚本序列里。说实话脚本 review 比代码 review 更严肃因为代码出问题还能秒回滚数据库结构变更改起来就是一阵大麻烦。宁可花十分钟慢慢看脚本也别上线后花一小时处理事故。8. 我的个人经验这半年用 Flyway 的变化最后聊点切身的感受。我所在的团队大概是从半年前开始在项目里全面推行 Flyway 的。起因是上线时连续两次因为数据库脚本问题导致发版回滚DBA 和开发互相甩锅。我实在受不了“手动执行脚本、群里喊话、环境状态说不清”的状态找了一天时间把所有环境接上 Flyway然后把团队内部的数据库变更流程全部重写了一遍。半年下来最直观的变化是上线时数据库层面几乎不需要额外关注了。以前发版前需要 DBA 出脚本清单核对每个环境手动执行现在 Flyway 自动搞定。以前有人随手改了一个历史脚本去了测试环境才发现不一致现在 checksum 校验直接挡在开发环境。以前新同事入职要花半天时间了解“哪些表在哪个环境执行过”现在查一张历史表就全清楚了。如果让我给还没接 Flyway 的团队一个建议我会这么说不要等到库表已经乱成一锅粥的时候再想尽早接入它给你的不只是自动执行更是整个数据库变更流程的秩序。存量项目也没有想象中那么难过渡baseline 一步先站稳后面再慢慢规范化就好。最后分享一个细节接入后记得在后端服务启动日志里加一个 Flyway 版本信息的输出这样每次发版前先看一眼日志确认迁移执行到了哪个版本心里就有底了。别小看这个动作很多生产问题其实在发版前看一行日志就能预防掉。