上周帮一个朋友排查线上问题,他们的订单表加了个索引,结果凌晨两点发版后,数据库直接锁了半小时。查下来,就是迁移工具在变更表结构时,没处理好锁机制。这种坑,我踩过不止一次。Java生态里数据库迁移工具不少,但选型这事,真不是看GitHub星星多就完事。

先说Flyway和Liquibase这两大主流。Flyway走的是SQL脚本路线,简单直接,版本号控制清晰,团队里只要有人会写SQL就能上手。Liquibase则用XML或YAML描述变更,好处是跨数据库兼容性强,但坏处也明显——抽象层会吃掉一部分SQL特性,比如PostgreSQL的某些特定类型,Liquibase可能就表达不了。我见过不少团队,项目初期选了Liquibase,结果后期要玩点复杂的窗口函数,还得回退到原生SQL。
这里有个关键坑:很多人在选型时只盯着功能对比,忽略了团队的实际水平。如果你的团队全是写业务代码的,对SQL并不精通,那Flyway的裸SQL反而成了负担。反过来,如果团队里有DBA坐镇,那Liquibase的建模能力就是加分项。工具是为人服务的,不是反过来。
再说说版本控制策略。Flyway用版本号排序,V1init.sql、V2addcolumn.sql这种命名方式,简单粗暴但有效。但有个细节容易翻车——如果你用了重复的版本号,Flyway会直接报错,这在多人并行开发时特别容易触发。我们之前有个项目,两个分支同时加了V3xxx.sql,合并时直接冲突,只能手动改版本号重跑。Liquibase用的是changelog文件加changeSet的id和author组合,理论上更灵活,但实际用起来,如果团队成员不遵守命名规范,changelog文件会变得一团糟。
这里建议:不管选哪个工具,都要在项目初期定好版本号命名规范,最好是加上日期或功能前缀,比如V20240115adduser_index.sql,这样既能避免冲突,也方便追溯。
接下来是执行时机的问题。很多团队把迁移脚本放在应用启动时自动执行,图省事。但这在微服务架构下就是个雷——多个实例同时启动,全都在跑迁移脚本,数据库连接池瞬间被打满,轻则脚本执行失败,重则把数据库拖垮。我们有个项目,就是因为这个原因,每次发版都要手动关掉几个实例,等迁移跑完再启动,麻烦得要死。
正确的做法是,要么用独立的迁移任务在发版前手动执行,要么在应用启动时加个分布式锁,保证同一时间只有一个实例在执行迁移。Flyway本身没有这个机制,需要自己实现。Liquibase有类似的功能,但配置起来也不简单。另外,迁移脚本的执行时间一定要监控起来,超过5分钟的迁移脚本,就要考虑是不是该拆分成多个小脚本了。
还有一个容易忽视的坑:回滚。Flyway不支持自动回滚,你得手动写undo脚本。Liquibase虽然支持rollback,但前提是你得在changeSet里定义好回滚逻辑。很多团队在实际操作中,回滚脚本写得极其粗糙,甚至不写。结果就是,一旦线上迁移出问题,只能靠DBA手动改数据库,风险极高。
我建议,凡是涉及删除列、修改类型、重建索引这种不可逆操作,一定要写回滚脚本,哪怕只是恢复成原来的结构。别以为测试环境跑通了就万事大吉,生产环境的数据量和索引分布,跟测试环境完全两码事。
再聊一个高级话题:影子表。有些团队为了保证迁移不阻塞线上业务,会用gh-ost或pt-online-schema-change这类工具,在影子表上先做变更,然后切换。但如果你用的是Flyway或Liquibase,它们本身不提供这个能力。所以,如果你的业务对可用性要求极高,比如金融交易系统,那单纯依赖Flyway或Liquibase是不够的,得额外引入在线DDL工具。
不过,这也有个权衡问题。影子表工具会消耗额外的数据库资源和磁盘空间,而且切换过程本身也有风险。我们之前给一个客户做过评估,他们的核心订单表有近亿条数据,建议他们直接通过运维窗口在低峰期执行迁移,而不是引入影子表,因为切换过程中的数据一致性校验,比迁移本身更复杂。
说点选型之外的思考。工具只是手段,真正重要的是你的迁移流程是否规范。我见过不少团队,迁移脚本写得跟意大利面条一样,靠注释硬撑,没有版本控制,没有review流程,上线全靠运气。这种团队,换什么工具都白搭。
我的建议是,先定好规范:迁移脚本必须经过代码评审,必须有对应的回滚方案,必须在测试环境完整跑一遍,生产执行时必须有人盯着日志。然后,再根据团队情况和项目复杂度,去选Flyway还是Liquibase。如果你只是个小项目,团队就三五个人,Flyway就够了,别折腾复杂的。如果你是个大型分布式系统,数据库种类还多,那Liquibase的抽象能力会帮你省不少事。
回到开头那个加索引锁表的案例。其实那哥们儿用的就是Flyway,问题出在他把一个大脚本拆成了几个小脚本,每个脚本里都带着ALTER TABLE,结果执行时,前一个脚本还没提交,后一个脚本就开始抢锁。这种问题,跟工具本身没关系,纯粹是脚本设计不合理。所以,选型归选型,但真正能帮你避坑的,还是你对数据库和迁移机制的理解深度。
工具是死的,人是活的。选型指南看得再多,不如自己动手在测试环境里跑几遍,模拟一下高并发下的迁移场景,感受下锁等待和连接池耗尽是什么体验。等你把这些坑都踩过一遍,你就知道该选什么工具,该怎么写迁移脚本了。


