简介:面向 Java 开发者的《DropwizardDB:Flyway 数据库迁移集成指南》PDF 文档,围绕在 DropwizardDB 项目中集成 Flyway 的完整流程展开,覆盖 DropwizardDB 与 Flyway 简介、开发环境搭建、Flyway 核心配置、SQL 迁移脚本编写、集成实施、测试验证、常见故障排查与最佳实践优化等模块,并细化数据库连接配置、脚本命名规范、事务处理、数据迁移、索引优化、监控日志等关键点,适合有一定 Java 基础、需要规范管理数据库版本与结构变更的后端开发者。压缩包共 1 个文件,PDF 格式,大小 4.62MB,单文件即包含全部 24 页内容;文档目录支持章节跳转和左侧大纲快速定位,配置参数、脚本示例、测试方法与回滚策略均可按需查阅。目前已有 63 人浏览学习,可直接作为 Dropwizard 项目引入 Flyway 的参考资料,帮助读者减少环境搭建与迁移脚本编写中的踩坑成本,提升数据库结构变更的可控性与可维护性。
1. 数据库迁移为什么容易翻车:DropwizardDB 与 Flyway 这套组合先救谁的场
上周线上服务发版,代码里新增了一个 phone 字段,数据库却没人动过,服务一启动查询全部报错。最后是运维手工执行 SQL,才把事故按下暂停键。这种事经历过一次,你就知道数据库迁移不该靠人肉记忆。
DropwizardDB + Flyway 就是用来解决这套问题的组合。DropwizardDB 是 Dropwizard 框架里负责数据库访问的那一层,把连接池、JDBI/Hibernate 和健康检查收拢好;Flyway 是 Java 社区用得最广的数据库迁移工具,把 SQL 脚本按版本号排好序,在应用启动时自动执行。两者一配合,数据库结构变更就从“发版前的玄学”变成了可回滚、可追踪的常规工序。
这篇笔记是写给有 Java 基础、正在用 Dropwizard 搭服务的开发者的。目标是让你看完就能在自己的项目里把 Flyway 接进去,并且知道迁移脚本怎么写、参数怎么调、哪个环节最容易踩坑。
2. DropwizardDB 与 Flyway 各管哪摊事:轻量框架为什么要配版本化迁移
2.1 DropwizardDB 是什么:不是数据库,是 Dropwizard 里的数据访问层
先解开一个误读。文档里反复出现的 DropwizardDB,并不是一个和 MySQL、PostgreSQL 并列的数据库引擎,它实际上是 Dropwizard 框架中负责数据访问的那一部分。Dropwizard 本身做的是 RESTful Web 服务的骨架:内嵌 Jetty,用 Jackson 处理 JSON,用 Metrics 做监控;而项目里要接数据库时,常用的是 dropwizard-jdbi3 或 Hibernate。很多人习惯把这一整套“配置数据源 + 连接池 + DAO + 事务管理”称为 DropwizardDB,它对应的就是工程里的数据访问层。
在微服务场景里,每个服务通常会连一个自己的业务库。服务一多,库结构就很容易漂移。开发环境改了一列,测试环境没同步,生产环境更是没人敢动。这时候 Dropwizard 的数据库配置本身不难,难的是让所有环境的结构保持一致。典型配置里,数据源相关参数集中在 config.yml:
database: driverClass: com.mysql.cj.jdbc.Driver user: your_username password: your_password url: jdbc:mysql://localhost:3306/your_database_name properties: charSet: UTF-8 maxWaitForConnection: 1s validationQuery: "/* MyService Health Check */ SELECT 1" validationQueryTimeout: 3s minSize: 8 maxSize: 32 checkConnectionWhileIdle: false evictionInterval: 10s minIdleTime: 1 minute其中 driverClass 必须和你引入的驱动 jar 匹配。MySQL 8 以上用 com.mysql.cj.jdbc.Driver,老版本有的是 com.mysql.jdbc.Driver,写错一个类名,服务启动时连池子都起不来。maxWaitForConnection 控制连接等待时间,validationQuery 让 Dropwizard 健康检查能探测数据库是否活着;minSize 和 maxSize 控制连接池大小,不是越大越好,后面调优部分我会单独说。
注意,DropwizardDB 能保证“应用连得上库、能操作数据”,却不能保证“库结构是正确的、可追溯的”。这正是 Flyway 要补的位置。
2.2 Flyway 的版本化迁移原理:schema history 表就是项目的账本
Flyway 的工作方式一句话就能说明白:把所有数据库结构变更写成 SQL 脚本,每个脚本带上唯一版本号和描述,执行时按版本号从小到大依次运行,并在一个账本表里记录下来。这个账本默认叫 flyway_schema_history,Flyway 每次启动都会先读它,确认哪些脚本已经执行过,哪些是新加的,然后只跑缺失部分。因为每个脚本名里带版本号,比如 V1__、V2__,它天然有顺序,不怕人多手杂。
账本表里最关键的几个字段是 version、description、type、script、checksum 和 success。我自己排查问题时,最常看的 SQL 是这一条:
SELECT installed_rank, version, description, type, success FROM flyway_schema_history ORDER BY installed_rank;这里 installed_rank 是实际执行顺序,version 是脚本版本号,success 字段表示该脚本是否成功。如果哪一次迁移脚本执行到一半报错,Flyway 会把这个版本标记为失败,同时把连接回滚到事务开始前。之后你再执行同样的脚本,它会继续按顺序跑,而不是把失败版本自动跳过。
严格说 Flyway 的“校验”机制也建立在这张表上。每个已执行脚本会记录内容校验和,如果你手工改过一个已经跑过的脚本,下一次启动时 Flyway 会报 Migration checksum mismatch。这不是误报,是防止有人偷偷修改历史迁移脚本后,在不同环境里跑出不同结果。这种事在多人协作的项目里几乎每周都会遇到,所以别去“骗”校验和,该加新版本就加新版本。
2.3 选型理由:不自己扫 SQL 脚本,而是交给 Flyway 管顺序和账本
也许你会说,我也可以写一个启动监听器,把 db/migration 目录下的 SQL 文件读完按文件名排序执行,不也一样吗?我曾经也这么干过,后来发现几个绕不开的痛点:普通文件名排序不等于语义上的依赖顺序,比如 V1.10 在字典序里排在 V1.9 前面,容易把新版本先执行了;SQL 失败后没有成功标记,下次启动又会跑一次已执行的部分;最致命的是,没有账本,你根本不知道这个库现在处于哪个版本,测试、预发布、生产环境的状态全靠猜。
Flyway 把这些都标准化了。migrate、validate、clean、repair 四条命令基本覆盖日常操作;脚本可以放在 classpath 里随应用发布,也可以指向外部目录;而且它对版本号采用“分段数字”排序,V1.10 会正确排在 V1.9 后面。这部分的规则后面讲脚本命名时还会再展开。总之,数据库迁移不是不能自己做,而是自己做容易漏掉边界条件。选 Flyway 的核心理由不是“它有 migrate 命令”,而是“它把顺序、状态、校验、回滚这些细节都收进了一个可审计的模型里”,这套模型正好补上 DropwizardDB 不管的那部分。
3. 集成准备的第一道坎:JDK、Maven、MySQL 和 flyway.properties 参数解析
3.1 JDK 与 Maven 环境:版本选择和 PATH 配置
整条迁移链路跑在 JVM 上,JDK 版本建议 8 以上,我自己现在默认用 JDK 11。版本不是越高越好,但太老的 JDK 会限制 Flyway 新版选择。配置环境变量的常见做法是把 JAVA_HOME 指向安装目录,再把 bin 加进 PATH。Windows 上通常在“系统属性 → 环境变量”里操作,macOS/Linux 则在 shell 配置里写:
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATH java -versionMaven 同样需要 M2_HOME 和 PATH 配置,配完以后用mvn -v验证。注意 Maven 3.8 以上对镜像仓库配置更严格,如果公司内部有私服,要在 settings.xml 里把 mirror 指向私服,否则后面拉依赖会一直卡住。
创建 Dropwizard 项目,可以用官方 archetype 直接生成骨架:
mvn archetype:generate \ -DarchetypeGroupId=io.dropwizard.archetypes \ -DarchetypeArtifactId=java-simple \ -DarchetypeVersion=2.1.2 \ -DgroupId=com.example \ -DartifactId=my-dropwizard-project \ -Dversion=1.0-SNAPSHOTWindows cmd 下多行命令要换成^连接,或者直接写成单行。这个 archetype 会生成一个带 Application 类、Configuration 类和 config.yml 的最小工程,省掉手动建目录的功夫。
3.2 MySQL 准备与连接串参数:驱动类、时区和连接池
数据库我一般选 MySQL 8,因为和 Flyway 8.x、Dropwizard 2.x 配合最顺。安装完成后,先在 pom.xml 里加驱动:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency>连接串参数里最容易忽略的是时区和 SSL 配置。MySQL 8 默认连 Java 驱动时,URL 不加 serverTimezone 会直接报时区错误;如果数据库和服务器不在同一时区,建议在 URL 上显式指定:
jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8useSSL=false 只建议在开发环境用,生产环境还是要把 SSL 打开,同时把用户名密码放到配置中心或环境变量里,不要硬编码进 config.yml。数据库账号也要注意权限,Flyway 要建表、改表、删表,用的账号至少得有 DDL 权限,否则第一个迁移脚本就会在 CREATE TABLE 上报 Access denied。
3.3 flyway.properties 核心参数:locations、table、baselineOnMigrate、outOfOrder
Flyway 的配置可以独立放在 src/main/resources/flyway.properties,也可以直接写在 Dropwizard 的 config.yml 里。独立文件更清爽,适合迁移逻辑和应用配置分离。下面是一份我用过的 MySQL 配置:
flyway.url=jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=Asia/Shanghai flyway.user=your_username flyway.password=your_password flyway.locations=classpath:db/migration flyway.table=flyway_schema_history flyway.baselineOnMigrate=true flyway.outOfOrder=false这里每个参数都有实际意义:
| 参数 | 默认值 | 作用 | 我的建议 |
|---|---|---|---|
| flyway.url | 无 | 目标数据库连接串,必须和 Dropwizard 数据源指向同一个库 | 不要拆成两部分写,容易漏参数 |
| flyway.locations | classpath:db/migration | 迁移脚本目录 | 默认即可,多模块时用逗号分隔多个路径 |
| flyway.table | flyway_schema_history | 版本账本表名 | 除非和已有表冲突,否则不用改 |
| flyway.baselineOnMigrate | false | 接管非空库时是否自动补基线 | 第一次接入存量库必须设 true |
| flyway.outOfOrder | false | 是否允许版本号乱序执行 | 正式环境保持 false,避免依赖反转 |
outOfOrder 这里要专门提醒:团队里两个人同时开发,一个提交了 V2,另一个提交了 V3,但 V3 先合入,配了 outOfOrder=true 还能继续跑,但长期会让环境状态难以预测。我在正式环境一律 false,宁可让发布卡住也不乱序执行。
3.4 第一个迁移脚本与最小验证:V1__、migrate() 和 users 表
迁移脚本命名格式是 V<版本号>__<描述>.sql,注意版本号和描述之间是两个下划线。版本号可以是 1、1.1、2.1 这样的格式,描述部分直接写这一版干了什么,比如 V1__Create_users_table.sql。下面这个脚本就是最基础的建表:
-- V1__Create_users_table.sql CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );写完脚本后,不要急着接 Dropwizard,先用一个独立 Java 类验证 Flyway 本身能不能跑通。这样后面集成时,你就知道问题出在 Flyway 还是 Dropwizard:
import org.flywaydb.core.Flyway; public class FlywayConfigTest { public static void main(String[] args) { Flyway flyway = Flyway.configure() .dataSource("jdbc:mysql://localhost:3306/testdb", "root", "root") .locations("classpath:db/migration") .load(); try { flyway.validate(); System.out.println("Flyway configuration is valid."); flyway.migrate(); System.out.println("Migration completed successfully."); } catch (Exception e) { System.err.println("Migration failed: " + e.getMessage()); } } }这段代码先 validate 再 migrate,是我的一贯习惯。validate 只检查配置和脚本是否一致,不会动数据;migrate 才会真正建表。如果 validate 过了但 migrate 报错,基本就是脚本本身的问题,排查范围会缩小很多。跑完后到数据库里查一下 users 表是否存在,顺手确认 flyway_schema_history 也生成了,再继续下一章。
4. 把 Flyway 真正装进 Dropwizard 项目:依赖、配置类与启动顺序
4.1 Maven 与 Gradle 依赖写法:版本锁死还是跟随 BOM
Flyway 单独配置跑通后,接下来才是集成。第一步是把依赖都放进 pom.xml,我一般会同时加 Dropwizard 核心、数据库访问模块、Flyway 和 MySQL 驱动四块:
<dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-jdbi3</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> <version>8.5.13</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> </dependencies>注意 flyway-core 8.5.13 这一代把核心功能都打在一个包里了。如果你用 Flyway 10 或更高版本,MySQL 方言被拆到了单独的 flyway-mysql 模块,只加 flyway-core 会报 “No database found to handle jdbc:mysql”。遇到这种报错,先检查是不是 Flyway 大版本升级后模块拆分导致,而不是怀疑数据库地址写错。
Gradle 项目的话,依赖写法对应调整:
dependencies { implementation 'io.dropwizard:dropwizard-core:2.1.2' implementation 'io.dropwizard:dropwizard-jdbi3:2.1.2' implementation 'org.flywaydb:flyway-core:8.5.13' implementation 'mysql:mysql-connector-java:8.0.26' }版本号我建议锁死到具体版本,不要用latest.release或者动态版本。迁移工具本身要稳定,动态版本会让线上环境变得不可控。
4.2 配置类与 FlywayBundle:在 Dropwizard 的 run() 里跑迁移
Dropwizard 集成 Flyway 的正规做法,是实现一个 ConfiguredBundle,在 bundle 的 run 方法里拿到 Configuration 实例和数据库参数,然后执行 migrate。这样迁移逻辑就嵌入了 Dropwizard 的启动生命周期,而不是散落在某个 Controller 里。
先扩展配置类:
import io.dropwizard.Configuration; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotEmpty; public class MyAppConfiguration extends Configuration { @NotEmpty @JsonProperty private String databaseUrl; @NotEmpty @JsonProperty private String databaseUser; @NotEmpty @JsonProperty private String databasePassword; public String getDatabaseUrl() { return databaseUrl; } public String getDatabaseUser() { return databaseUser; } public String getDatabasePassword() { return databasePassword; } }然后实现 FlywayBundle:
import io.dropwizard.ConfiguredBundle; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; import org.flywaydb.core.Flyway; public class FlywayBundle implements ConfiguredBundle<MyAppConfiguration> { @Override public void initialize(Bootstrap<?> bootstrap) { // 这里只做初始化,不涉及数据库连接 } @Override public void run(MyAppConfiguration configuration, Environment environment) { Flyway flyway = Flyway.configure() .dataSource( configuration.getDatabaseUrl(), configuration.getDatabaseUser(), configuration.getDatabasePassword()) .locations("classpath:db/migration") .baselineOnMigrate(true) .load(); flyway.migrate(); } }最后在 Application 里把 bundle 挂进去:
import io.dropwizard.Application; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; public class MyApplication extends Application<MyAppConfiguration> { public static void main(String[] args) throws Exception { new MyApplication().run(args); } @Override public void initialize(Bootstrap<MyAppConfiguration> bootstrap) { bootstrap.addBundle(new FlywayBundle()); } @Override public void run(MyAppConfiguration configuration, Environment environment) { // 注册 Resource、HealthCheck 等 } }为什么放在 ConfiguredBundle 而不是直接写在 Application.run 里?因为我希望迁移逻辑和应用业务初始化解耦。bundle 的 run 方法会在 Dropwizard 构建 Environment 之后、正式对外提供服务之前执行,这样能保证 Jetty 端口都还没监听时,数据库结构已经到位。如果写进某个 Controller 的构造函数,就可能出现接口都注册了、迁移还没跑完的情况,用户请求一进来就开始查不存在的表。
4.3 脚本加载与版本管理:classpath 路径和命名统一
Flyway 默认从 classpath:db/migration 找脚本,所以只要把 SQL 文件放在 src/main/resources/db/migration 目录下即可。但要注意,Maven 多模块项目里,迁移脚本放在哪个模块很讲究。我的习惯是单独拆一个 db-migration 模块,专门放 SQL 脚本,业务模块通过依赖引用它。这样数据库变更记录可以独立走版本管理,不随业务代码一起混乱。
如果既有 classpath 脚本又有外部脚本,locations 参数支持逗号分隔:
flyway.locations=classpath:db/migration,filesystem:/data/migration外部目录的好处是 DBA 可以直接维护服务器上的脚本,坏处是版本和代码仓库不同步。我一般只在需要热修数据库时才用外部目录,日常统一走 classpath。
版本管理这里还要提一个坑:同一个版本号不能出现在两个脚本里。比如先提交了 V2__Add_age.sql,后来又手滑提交了一个 V2__Add_status.sql,Flyway 启动时就会报重复版本。命名规范定下来以后,建议在团队的 CI 里加一个检查脚本,把所有文件名扫一遍,重复版本号直接让构建失败。
4.4 集成测试与单元测试:别让迁移变成一个黑匣子
集成做完以后,最怕的是迁移从来没在真实 MySQL 上跑过。我建议至少写一个集成测试,用测试库执行完整迁移,然后断言关键表存在:
import org.flywaydb.core.Flyway; import org.junit.jupiter.api.Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import static org.junit.jupiter.api.Assertions.assertTrue; public class MigrationIntegrationTest { @Test void migrateShouldCreateUsersTable() throws Exception { Flyway flyway = Flyway.configure() .dataSource("jdbc:mysql://localhost:3306/testdb", "root", "root") .locations("classpath:db/migration") .load(); flyway.migrate(); try (Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/testdb", "root", "root"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT COUNT(*) FROM information_schema.tables " + "WHERE table_schema='testdb' AND table_name='users'")) { rs.next(); assertTrue(rs.getInt(1) == 1, "users 表不存在,迁移未生效"); } } }这段测试的好处是它会在同一个事务里完成迁移和验证,一旦脚本里出现低级错误,比如表名拼写不一致,测试直接失败。把这类测试挂在 CI 上,每次合并代码都跑一遍,比发布后再查数据库靠谱得多。
单元测试层面则不用真的连数据库,只需要验证 Flyway 配置对象的 locations、baselineOnMigrate 等参数是不是预期的值。集成测试负责“能跑”,单元测试负责“配置没丢”,两层合起来,迁移这块才算有基本保障。
5. DropwizardDB 集成 Flyway 的常见问题排查:五张现场事故单
5.1 迁移脚本执行失败:SQL 语法错误与版本顺序
现象:应用启动时报Migration V2__Add_email_column_to_users_table.sql failed,日志里能直接看到 SQLException。
原因:SQL 语法错误最常见,其次是脚本里引用了不存在的表,比如 V2 想给 users 表加字段,但 V1 建的表名拼成了 user,V2 自然执行失败。另一种情况是版本顺序没对齐,例如 V1.1 在 V2 之后才提交,outOfOrder 又没开。
解决:先用本地库单独执行该版本的 SQL 文件,用 mysql 客户端直接跑一遍,排除语法和表名问题。确认 SQL 没问题后,如果这次失败是因为版本顺序,先看历史表里已执行的版本和文件列表,规划好新版本号再提交。注意 Flyway 不会自动跳过失败版本,你必须修复后重新 migrate,它会在失败事务基础上继续,而不是重复已经成功的部分。
5.2 数据库连接失败:驱动、时区与权限的三连击
现象:日志里连续出现Communications link failure或者Unable to obtain connection from database,但数据库服务明明是在跑的。
原因:这类问题我排查下来有三个高频来源。一是 driverClass 写错,MySQL 8 驱动类必须是 com.mysql.cj.jdbc.Driver;二是 URL 没带 serverTimezone,MySQL 8 连接时报时区错误;三是数据库账号权限不足,Flyway 需要 DDL 权限,但给的是只读账号。
解决:把 config.yml 里的 url、user、password 和 flyway.properties 里的配置逐项对比,确保两项指向同一个库、同一套账号。如果是一个集成环境里两个配置串混了,出现“应用连 A 库、Flyway 迁 B 库”的割裂状态,启动时 Flyway 跑完,应用却还在查旧表。最稳的做法是把 URL、账号、密码统一放到环境变量里,两边都读取同一个变量。
5.3 flyway_schema_history 表异常:checksum 失败、损坏与误删
现象:启动时报Migration checksum mismatch for migration version 2,或者报No migration found for schema history table。
原因:前者是有人改过已经执行过的历史脚本,Flyway 发现文件内容和历史表里记录的 checksum 对不上。后者更常见,某次上线时 DBA 手误把 flyway_schema_history 表删了,或者有人手工执行了 DROP TABLE,Flyway 找不到账本,就认为这是一个全新库,会试图把所有脚本重新执行一遍。
解决:checksum mismatch 不能靠 repair 硬解。正确做法是判断这次改动是否必要:如果只是改了注释,就把历史表里对应版本的 checksum 用 repair 更新;如果真改了表结构逻辑,应该新增一个更高版本的迁移脚本,而不是改旧文件。历史表被误删时,如果已经执行过的脚本内容还保留在代码库里,可以用 baselineOnMigrate 重新基线,或者从备份里恢复这张表。血的教训是:永远不要把 flyway_schema_history 当成普通日志表去清理。
5.4 并发迁移:多实例启动时锁冲突与重复执行
现象:服务以多实例部署,同一时间多个节点同时启动,日志里出现Your database servernot reachable或Unable to obtain exclusive access to the database。
原因:Flyway 在迁移时会尝试获取迁移锁,保证同一时间只有一个实例在改表。多个实例同时跑 migrate,就会互相争抢锁。另一个相关场景是任务调度器重复触发迁移,或者有人手动执行了两次 migrate。
解决:最稳妥的办法是把迁移从一个独立的维护任务来触发,比如单独的命令行入口,而不是每个应用实例启动时都跑。如果团队坚持应用启动时迁移,那么至少要在 Flyway 配置里打开重试或让应用启动失败后快速回滚,避免半启动状态对外提供流量。我在生产环境一般让迁移在部署流水线里一次性执行,应用实例启动时只做 validate,不做 migrate,这样并发问题直接绕开。
5.5 启动时序问题:migrate 已经执行,应用却查不到表
现象:日志里明明打印了Migration completed successfully,但业务接口一调用就报“Table 'xxx' doesn't exist”。
原因:这往往是 Dropwizard 和 Flyway 使用了不同的数据库连接串。Flyway 成功迁移的是 A 库,应用数据源配置却指向 B 库;或者两者连接同一个实例但 databaseName 大小写不一致,Linux 下 MySQL 表名是区分大小写的。还有一种情况,迁移发生在第一个请求之后,比如把 migrate 写进了某个 Resource 的构造函数,这时 Jetty 已经在监听端口了。
解决:先确认两个连接串指向同一个库,最简单的验证方式是在 migrate 之后打印当前连接的 database()。再看启动顺序,把 FlywayBundle 在 initialize 里通过 addBundle 挂上,确保 migrate 在 Environment 正式对外服务之前跑完。最后检查表名大小写,统一用小写,省得跨环境翻车。
6. 进阶:让迁移脚本可回滚、可监控、可复用
6.1 回滚策略:不要指望 Flyway 默认给你 undo
Flyway 的 migrate 是“只进不退”的,没有默认的 rollback 命令。它能保证失败事务回滚,但不能保证版本降级。所以生产环境里要准备一套手动回滚方案,我的习惯是给每个破坏性迁移脚本配套一个反向脚本,放在 migrations/undo 目录里,命名上用 U 前缀标记。比如 U2__Drop_email_column.sql 要对应 V2 的加列操作。这个脚本不会自动执行,但真出问题时它就是后悔药。
6.2 迁移耗时监控与连接池调优
迁移脚本多了以后,执行时间会变长。我建议在每个迁移脚本里记录预期耗时,并让 Flyway 的迁移过程输出到 Dropwizard 的日志体系里。性能问题通常集中在数据迁移脚本,一条 INSERT INTO SELECT 在百万行上可能跑几分钟。这时候把事务拆小,分批提交,比一条大事务更安全。连接池参数上,minSize 保持 8、maxSize 不要一下子拉太高,否则 MySQL 的连接数会被十几个服务实例打爆,迁移过程中也会因为连接等待超时。
6.3 一个低成本的表结构验证技巧
发版前最怕的就是“测试环境能跑,生产环境炸了”。我的笨办法是,准备一个空库,跑完所有迁移脚本后,导出表结构清单,再和线上库的表结构做 diff,只对比表名、字段、索引。用工具生成结构差异,比人眼一个个看可靠得多。从那以后,我每次发版前都强制走一遍这个流程:拉起空库 → 跑全量迁移 → 结构对比 → 确认无差异,宁可多花十分钟,也不想去生产库和同事一起猜。希望帮到你。
本文还有配套的精品资源,点击获取