ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flyway数据库版本管理:从零实践到生产环境避坑指南

Flyway数据库版本管理:从零实践到生产环境避坑指南 接手过老项目的朋友应该都经历过这种场景本地跑得好好的一上测试环境就报字段不存在生产库不知道被谁改了表结构新同事入职第一件事是从线上导一份表结构才能把开发环境搭起来。这些问题归根到底就是一句话数据库结构变更没有纳入版本管理。Flyway就是专门解决这件事的开源工具它把每一次数据库结构变更都写成脚本像管理代码一样管理变化从根本上实现数据库版本自动化管理。无论你是刚入行的后端开发还是维护了多年老系统的架构师只要你的项目离不开数据库这篇文章都值得你花十分钟读完。1. 为什么数据库结构需要一套版本管理——Flyway的核心设计思路1.1 代码有Git管数据库结构谁来管做过几年开发的人都有种感觉代码的版本管理早已是标配但数据库的版本管理却常常处在原始社会。Git管得住Controller、Service、Mapper但管不住CREATE TABLE、ALTER TABLE、INSERT INTO ...。结果就是代码仓库里一份建表SQL本地库一套测试环境一套生产环境又一套谁也不敢保证这三套完全一致。我见过最典型的现场某功能在测试环境一切正常上线前DBA手工在生产库执行了几个补丁脚本结果漏了一个ALTER TABLE线上接口全部报ORA-00942。代码回滚没用因为结构已经变了。这种事故的本质不是某个人粗心而是数据库结构的变更流程缺少一个只要执行过、永远不会再执行的机制。Flyway解决的就是这一件事把数据库结构当成一等公民纳入版本管理。它不替代Git也不替代DBA它管的是结构变更的版本化执行过程——谁改的、改了什么、执行到哪一步、当前库处于哪个版本一目了然。1.2 把SQL脚本变成数据库的提交记录Flyway的设计思想说穿了特别朴素每次结构变更就是一个带版本号的SQL脚本。工具启动后会自动检查当前数据库已经执行到哪个版本然后按顺序执行还没跑过的脚本并把每一次执行记录到一张历史表里。这个过程是不是很像Git提交Git用commit记录代码变更Flyway用migration记录数据库结构变更。Git有提交历史Flyway有flyway_schema_history历史表。Git可以checkout到任意版本对比差异Flyway可以info查看当前库处于哪个版本。理解了这个类比Flyway的绝大部分概念你都能秒懂。从代码实现角度看Flyway本身是一个Java工具但它的执行方式非常灵活可以通过命令行CLI跑可以通过Maven/Gradle插件跑可以嵌入Java应用在启动时自动跑还可以通过Docker镜像在CI流水线里跑。无论哪种方式核心逻辑都一样连接数据库查历史表比对版本按序执行增量脚本。这种连接即执行的设计让它天然适合部署流程的自动化。1.3 Flyway工作流程与核心概念速览在进入实操前先把Flyway的几个核心概念串一遍后面讲到代码你才不会懵。概念作用类比Migration迁移脚本每一次结构变更对应的SQL或Java脚本Git的一次提交Version版本号脚本的唯一标识决定执行顺序提交号flyway_schema_history记录所有执行过的脚本及校验信息Git提交历史Validate校验检查已执行脚本是否被修改过检查提交是否被篡改Baseline基线给已有数据库打一个起点版本标记给现有代码做初始提交Repair修复修复历史表与脚本不一致的状态修正提交历史Clean清理清空整个库结构一般只用于开发环境删库重来整个执行流程可以概括为Flyway连接数据库锁定历史表读取已执行版本扫描脚本目录中的待执行脚本按版本号排序逐个在事务中执行然后写入执行记录。如果中间任何一个脚本失败默认会回滚当前脚本在支持事务的数据库上并停止后续执行等待人工处理。2. 上手前必须吃透的细节脚本命名、历史表与迁移类型2.1 脚本命名规则一步错步步错Flyway对脚本文件名有严格约定新手踩坑最多的就是这里。标准格式是前缀 版本号 分隔符 描述 后缀以V2__add_user_table.sql为例V是前缀表示这是一个版本化迁移2是版本号__是分隔符两个连续下划线add_user_table是描述.sql是后缀。这套命名规则没有商量的余地文件名写错了Flyway要么不识别要么识别出奇怪的结果。版本号支持点分形式比如V1.1__xxx.sql、V1.2__xxx.sql。排序时是按点分逐段比较的所以V1.10会排在V1.9后面这一点和字符串排序不一样别拿String.compareTo的逻辑直接套。版本号一旦执行过就永远不能改——改了之后checksum校验会失败后面细说。还有一个高频坑SQL脚本的编码。我遇到过同事用Windows记事本保存脚本带了BOM头Flyway执行时报解析错误。建议所有脚本统一UTF-8无BOM并在团队里约定好编辑器配置。2.2 flyway_schema_history 这张表到底记了什么每次成功执行一个迁移脚本Flyway都会往flyway_schema_history表插入一条记录。这张表通常包含以下核心字段字段含义installed_rank执行顺序编号从1递增version迁移版本号可重复迁移脚本这里为空description脚本描述部分type迁移类型SQL、JDBC、SPRING_JDBC等script脚本文件名checksum脚本内容的CRC32校验和installed_by哪个数据库用户执行的installed_on执行时间execution_time执行耗时毫秒success是否成功1/0这张表就是Flyway判断哪些脚本该执行的唯一依据。启动时它先查这张表拿到当前最大版本号然后只执行版本号大于它的脚本。所以这张表一定要保护好不要手动删数据、改数据一旦历史表与实际状态不一致Flyway会认为库是全新的把所有脚本重跑一遍结果就是各种对象已存在的报错。一条我自己的经验接入Flyway的项目上线排查问题先看这张表。success1且version连续的说明结构变更都正常执行了中间如果缺了某个版本马上就能定位是哪个脚本跳过了。2.3 三种迁移类型V、U、R怎么选Flyway支持三种前缀的迁移脚本V版本化迁移、U撤销迁移、R可重复迁移。很多人只用了V其实R在实际项目里非常有用。V前缀的脚本只执行一次执行记录永久保留在历史表中用于创建表、加字段、加索引等结构性变更。一个原则一旦执行过这个文件就封存了永远不要再动它。R前缀的可重复迁移脚本每次启动都会执行且会记录checksum用于视图、存储过程、函数这类每次启动都希望保持最新定义的对象。文件名格式如R__user_view.sql注意没有版本号部分。每次执行后历史表里会新增一条version为空的记录。因为每次都跑性能敏感的SQL别放这里并且要注意执行顺序R脚本是在所有V脚本执行完之后才执行的。U前缀的撤销迁移用来回滚对应的版本化迁移。听起来很诱人但我在生产环境几乎不用。原因很简单数据库 DDL 大多不可逆删掉的字段数据找不回来而且撤销脚本本身也是脚本也可能写错。日常开发中更稳妥的回滚方式是写一个新的V脚本把结构改回去而不是执行U去倒退版本。U脚本适合的场景大概只有开发环境想快速清掉某个实验性变更。3. 从零到生产Flyway接入的完整实操过程3.1 Spring Boot项目快速接入我接触的项目里大部分是Spring Boot应用接入Flyway这也是最省事的方式。由于Spring Boot对Flyway做了自动配置引入依赖后基本零配置就能跑起来。在pom.xml里加依赖dependency groupIdorg.flywaydb/groupId artifactIdflyway-core/artifactId version9.22.3/version /dependency如果是MySQL 8、PostgreSQL等新版本数据库还要额外加上对应的数据库模块比如MySQL需要加flyway-mysql。不加的话启动时会报数据库不受支持的错这是我第一次接入时踩过的坑后来发现官方文档明确写了从Flyway 8.2开始MySQL模块被独立拆分了。接着在application.yml里配置spring: flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: truelocations指定脚本目录默认就是classpath:db/migration不配也行。baseline-on-migrate是个安全开关后面讲存量项目时细说。配置完后在src/main/resources/db/migration下放脚本启动应用Flyway会自动执行迁移日志里会看到Successfully applied 3 migrations之类的输出。如果你是普通Java项目而非Spring Boot或者想用命令行操作可以用Flyway官方CLI加flyway.conf配置文件核心配置项是flyway.urljdbc:mysql://localhost:3306/myapp flyway.userroot flyway.passwordxxxx flyway.locationsfilesystem:/opt/sql3.2 新项目从零开始第一次迁移这样写新项目没有历史包袱从建库开始就把Flyway纳入流程体验是最顺畅的。我一般会这样做。第一步写第一个脚本V1__init_schema.sql把最基础的表结构建好CREATE TABLE user_account ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL, email VARCHAR(128), status TINYINT DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE user_login_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );第二步启动应用观察日志。正常流程是Flyway创建历史表执行V1脚本插入历史记录。执行完打开数据库看两样东西业务表建好了flyway_schema_history里有一条V1的记录。第三步当需求来了需要加字段时新建V2__add_user_profile.sql比如ALTER TABLE user_account ADD COLUMN nickname VARCHAR(64); ALTER TABLE user_account ADD COLUMN avater_url VARCHAR(255);注意加字段这种变更永远是新脚本新版本绝不回头改V1。这里有个我自己的习惯把avatar_url误写成avater_url这种拼写错误一旦执行了就只能靠后续脚本修正。所以写脚本时字段名、表名最好多检查几遍别依赖错了再改。新项目的核心要点是版本从1开始连续递增不允许出现V2执行完了再冒出一个V1__other.sqlFlyway会报Detected resolved migration not applied to database因为它发现脚本目录里存在一个版本号比当前库版本低的脚本。3.3 存量项目安全接入baseline的正确姿势大部分真实项目不是新项目数据库里已经有一堆表了这时直接放一个V1__init.sql会执行失败因为表已存在。正确的做法是使用baseline给存量数据库定一个起点版本。操作分三步。第一步在脚本目录放一个空的基线脚本V1__baseline.sql里面可以是空内容或一行注释。第二步配置基线版本并执行flyway baseline -baselineVersion1如果是Spring Boot项目配spring.flyway.baseline-version: 1首次启动时自动完成baseline。第三步后续新变更从V2开始写。执行完成后历史表里会有一条version1, typeBASELINE的记录代表这个库的初始状态就是版本1Flyway不会再尝试执行版本号小于等于1的脚本。baseline其实就是在告诉Flyway现有库结构我认账了你从V1之后开始管理。有一个决策点基线版本号应该大于当前所有已有脚本版本。如果你们团队之前用过其他数据库变更工具或者手工维护过一堆带编号的SQL文件建议把基线版本设为比现有最高编号再高一位避免冲突。我在存量项目上还做过一种稳妥操作先把所有存量schema的DDL导出整理好放进V1然后用baseline-version0的方式接入。这样历史表里 baseline 版本是 0V1会真正执行。但前提是V1里的DDL必须和当前库结构完全一致否则执行时会因为对象已存在而失败。总体来说baseline方案最省事也最安全除非你有把握导出脚本能原样重建整个库。3.4 多环境部署与发布节奏开发、测试、生产多环境部署正是Flyway发挥最大价值的地方。理想流程是这样开发者在分支上写新脚本合并到主干后CI阶段用测试库跑一次完整迁移验证脚本正确性发布时会分别在测试环境、预发环境、生产环境按顺序执行同样的脚本每个环境的历史表独立记录各自的执行状态。这里有个容易被忽视的细节Flyway脚本是一套但环境配置不同。生产环境的数据库账号通常只有DDL/DML权限没有修改flyway_schema_history的额外风险正常跑没问题。但有些公司生产库由DBA手工把关不允许应用自动执行DDL这时可以把Flyway配置成只生成SQL由DBA审核后手动执行flyway migrate -dryRunoutput.sqldryRun模式会生成将要执行的SQL而不真正执行这是生产环境接入Flyway时一个很实用的折中方案。既能审查内容又能保持流程自动化。发布节奏上我有一个强烈建议数据库迁移要和代码发布解耦。最安全的模型是先迁移后部署也就是应用启动前先执行flyway migrate确保数据库结构已经就绪再发布新版本代码。如果顺序反过来新代码先上线访问一个还没有新字段的表线上故障分分钟出现。这块可以直接写进CI/CD流水线比如在Jenkins或GitLab CI里构建后先跑Flyway迁移任务成功后再部署应用。3.5 多分支并行开发时版本号怎么协调团队多人并行开发每个人都在写自己的分支各自提交了V2__xxx.sql合并主干时就会遇到版本号冲突。我目前用过比较顺的方案是约定版本号不能用当前主干版本1而是用日期序号或主干版本分支标识的方式避免冲突。比如主干最新是V5分支A用V5.1__add_a.sql分支B用V5.2__add_b.sql。点分版本的好处是互不阻塞合并后Flyway会按V5.1、V5.2的顺序执行。如果两个分支都用了V6合并后就只能手动改掉其中一个的版本号。另一个常见做法是CI合并时统一改版本号。主干上有个MR触发CI自动检查所有待合并脚本的版本号是否冲突冲突就报错并提示修改。这个检查不复杂写个脚本读一下脚本文件名集合就行但能省掉很多脑细胞。说到底版本号冲突不是Flyway的问题是协作流程需要定规矩的问题提前约定比事后补救成本低得多。4. 实战排坑常见问题与处理技巧实录4.1 checksum mismatch脚本被改过的处理这是Flyway最常见的报错报错内容类似Migration checksum mismatch for migration version 3意思是V3脚本执行过之后文件内容被人改过了。Flyway用checksum把脚本内容指纹记录在历史表里启动时重新计算比对不一致就拒绝执行。这个机制本质是好事它挡住了改历史脚本这种高危操作。但很多时候脚本被改是开发环境中的无心之失。比如V3执行后开发者发现SQL写错了一个字段名直接改了原文件而不是新增脚本本地Flyway校验立刻报警。处理方式看环境如果只在开发环境且确认数据库结构还没有被后续版本依赖可以执行flyway repair它会更新历史表中的checksum使记录与新文件内容一致如果脚本已经在测试或生产环境执行过repair就不能解决问题了正确做法是恢复原文件内容把正确变更放到新版本脚本里。我经历过一次惨痛教训同事在生产库直接执行了repair把一个被误改的V3脚本的checksum更新了但数据库结构实际还是旧版导致后续V4里对新字段的操作全部失败。修复工作花了整整一下午。所以repair只在本地开发环境用生产环境任何与校验相关的异常都要先人工核对数据库实际状态再决定下一步。4.2 迁移失败与事务陷阱Flyway默认在事务中执行迁移脚本一个脚本失败整个脚本回滚。但这里有个巨大的盲区MySQL的DDL语句不支持事务回滚。也就是说CREATE TABLE、ALTER TABLE这类语句一旦执行即使后面SQL报错回滚表结构也已经改了。举个例子一个脚本里写了三步先CREATE TABLE a再INSERT 数据最后ALTER TABLE b失败。MySQL环境下前面的建表和插入如果已经提交是无法整体回滚的。Flyway检测到失败后会把历史表中对应的记录标记为success0后续启动时会一直报Detected failed migration拒绝执行任何其他脚本。处理办法是第一一个脚本只放一个不可分割的DDL操作降低失败面第二尽量把DDL和DML拆开DDL脚本不做数据变更第三遇到失败的中间状态先人工修复数据库到预期结构再执行flyway repair清除失败记录最后继续。PostgreSQL和Oracle这类支持DDL事务的数据库体验好很多一个脚本就是一个事务失败自动回滚不会留下半截状态。4.3 与JPA/Hibernate的ddl-auto冲突Spring Boot项目里同时开着spring.jpa.hibernate.ddl-autoupdate和Flyway等于两个工具在抢数据库结构控制权。Hibernate按实体类推断表结构Flyway按脚本执行变更两边定义不一致时先跑的那个会决定实际结构后跑的要么报错要么悄悄改掉。标准做法是ddl-auto在生产环境设为validate或none所有结构变更交给Flyway管理。开发环境想图省事用update也得注意顺序——Spring Boot默认Flyway先执行然后Hibernate再启动如果脚本里表结构比实体类旧Hibernate的update可能又会补上字段导致测试环境结构与生产不一致。在这个问题上我的经验是放弃ddl-autoupdate彻底让Flyway成为唯一的结构变更入口。开始时稍微麻烦但换来的是各环境结构完全一致排查问题时不用再猜是不是Hibernate偷偷加了个字段。你的实体类只管查数据别管建表。4.4 高频问题速查表与避坑清单把这几类高频问题整理成速查表遇到类似报错先对照检查报错或现象可能原因处理方向Detected resolved migration not applied脚本目录出现比当前库版本低的未执行脚本检查新增脚本版本号改到当前版本之后Migration checksum mismatch已执行脚本内容被修改确认环境本地用repair生产恢复原文件Detected failed migration上一次迁移失败留下标记修复数据库实际状态后repairSchema history table is corrupted历史表被人工改动基于备份恢复切勿手动编造记录Non-empty schema without schema history table库里有表但没有历史表配置baseline-on-migrate或手动baselineUnsupported Database缺少对应数据库模块依赖按数据库类型添加flyway-mysql/flyway-postgresql等避坑清单都是真金白银换来的脚本命名只用V数字__描述.sql不要用时间戳。任何执行过的脚本不得修改哪怕只改一个空格。每个脚本只做一件事DDL和DML分开。生产库执行前先备份最好用dryRun预览SQL。历史表视为只读状态表任何手工修改都可能是事故源头。CI流水线中迁移步骤放在部署步骤之前。新成员入职第一课Flyway脚本是数据库的唯一真相日常改结构先加脚本不要连库操作。4.5 我踩过的一个小事值得单独说有一次我为某个表加了一个非空字段脚本写的是ALTER TABLE user_account ADD COLUMN nickname VARCHAR(64) NOT NULL;执行时直接失败——表里已经有历史数据了加非空字段默认值都没有数据库自然拒绝。当时我的处理是改成两步先加可空字段再用默认值回填最后改成非空。这个教训提醒我非空字段上线前一定要想清楚存量数据怎么处理。这也是Flyway这类工具给我们的隐形价值——脚本执行失败会立刻暴露结构变更里没考虑周全的地方早炸比上线后才炸好受得多。后来我把变更脚本三问写进了团队规范这条变更对存量数据有什么影响是否需要回填数据是否需要先可空后非空的两步走每题想清楚了再写脚本数据库事故率明显下降。写在最后的一点体会从第一次在项目里引入Flyway到现在最直观的感受是排查环境不一致这类问题的电话少了很多。以前上线最紧张的环节之一就是谁去执行数据库脚本现在流水线里自动跑执行记录、执行人、执行时间全部留痕。个人经验是接入Flyway本身不难难的是让团队接受那个脚本一旦发布就不许改的纪律。Flyway只是工具真正起效的是围绕它建立的那套变更流程。如果你正在为多环境数据库结构不一致而头疼建议从今天起把第一个版本化脚本写下来剩下的就是坚持。
返回列表