ARTICLE DETAIL

资讯详情

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

Liquibase实战:从核心原理到SpringBoot集成,一文搞定数据库版本管理

Liquibase实战:从核心原理到SpringBoot集成,一文搞定数据库版本管理 1. 为什么我给团队强制引入了 Liquibase先讲一次差点背锅的经历先说一个真实的事。有一年我负责一个老项目的升级改造数据库里有张订单表要加一个user_coupon_id字段。开发环境是我加的测试环境是我同事加的生产环境上线当天 DBA 手动执行了一遍脚本。结果下午业务方就反馈部分老订单查不到优惠券信息。查了半天才发现开发环境和测试环境因为反复改字段定义列名和注释对不上生产环境 DBA 执行脚本时多跑了一遍幂等性没处理好的更新语句。三个环境的表结构长得跟三个不同妈生的一样。这种问题在没引入数据库版本管理工具的团队里几乎天天都在发生。你们有没有这种经历接手一个老项目IDE 里代码倒是能跑但一连上本地数据库就报字段不存在或者上线前有人问你这个字段你加到测试环境了吗你一愣发现自己只在开发环境手动加过。那段时间我就在团队里硬推了 Liquibase。这东西不是什么黑科技它的定位特别朴素把数据库的结构变更也当成代码一样纳入版本管理。你写了个 Java 类改了代码用 Git 提交大家拉下来一致数据库这边Liquibase 就是那套给数据库用的Git。它把你的建表、加字段、加索引、改注释、插入初始化数据等等操作都写成有版本、有作者、有编号的变更集changeset放进项目里统一管理。应用启动时框架会自动比对当前数据库实际结构和变更记录按顺序执行你没执行过的变更。这个工具对谁最有用我觉得是三类场景多人协作的后端团队——尤其是前后端分离、后端每个人都有自己的本地库那种生产环境变更审批严格的项目——因为所有 DDL 都在代码里DBA 审查变的非常轻松还有就是需要多环境保持一致的部署流程——从开发到测试到生产一套脚本推到底。当然任何工具都有学习成本。Liquibase 的入门曲线不算陡但坑是真不少。接下来的内容我会从核心原理讲到实际落地配置再把我踩过的大坑和团队执行规范一次性分享出来。如果你正在考虑要不要引入或者已经引入但被各种报错折磨这篇文章应该能帮你省不少时间。2. 理解这三个核心概念你就掌握了 Liquibase 的一半先别急着写配置。我见过太多人上来就复制一段 yml跑通了就以为会了结果一遇到 checksum 校验失败、changelog 锁死、回滚失效之类的问题就抓瞎。这些东西的根源全在对 Liquibase 三个核心概念的理解上。2.1 changelog数据库变更的总账本changelog 是 Liquibase 的入口文件它本身不直接写 SQL它负责描述按什么顺序、执行哪些变更。你可以把它理解成一个总账本里面记录了一条一条的变更条目。Liquibase 支持 XML、YAML、JSON、SQL 四种格式来编写 changelog。在 SpringBoot 项目里默认约定是把主 changelog 放在classpath:/db/changelog/db.changelog-master.yaml或.xml。你可以在主 changelog 里通过include标签引入子文件也可以用includeAll把一个目录下的所有 changelog 文件按文件名顺序全部引入。这点非常像代码里拆模块——主入口只负责编排实际变更拆到不同文件里按功能或时间组织。我团队里现在的目录结构大概是这样的src/main/resources/db/changelog/ ├── db.changelog-master.yaml ├── v1.0.0/ │ ├── 001-create-order-table.yaml │ ├── 002-add-user-coupon-column.yaml │ └── 003-init-dict-data.sql ├── v1.1.0/ │ └── 001-add-payment-log-table.sql └── v1.2.0/ └── ...主文件里按版本号引入子目录每个版本目录里再按序号拆分文件。这个结构的好处是链路清晰code review 的时候扫一眼就知道某个版本改了哪些东西回滚也能精确到版本。2.2 changeset最小变更单元idauthor 是它的身份证changelog 里一个changeSet或者 yaml 里的- changeSet:就是一次最小变更。每个 changeSet 必须有id和author这两个字段合起来组成它的唯一标识。- changeSet: id: create-order-table author: zhangsan changes: - createTable: tableName: t_order columns: - column: name: id type: BIGINT autoIncrement: true constraints: primaryKey: true nullable: false注意这里的id不要理解为数据库里那个自增主键它更像是一个业务上的提交编号。你可以用功能名add-user-coupon-column也可以用日期加序号20250115-01只要在同一个 author 下不重复就行。这里有个极其重要的特性我必须提醒你一旦某个 changeSet 被执行过它的内容就不能再修改了。Liquibase 会在数据库中记录这个 changeSet 执行完的 MD5 校验值。下次启动时它会重新计算 changelog 文件里的内容哈希跟数据库里的记录做比对不一致直接就报错Validation failed: 1 change sets check sum mismatch。这不是 Bug这是特性它的目的是防止有人在已经上生产环境的脚本上偷摸做修改。所以正确的做法是如果之前的变更写错了不要改旧 changeSet新写一个 changeSet 去修正。比如你给表加了字段后来发现类型写错了那就再写一个modifyDataType或者dropColumn加addColumn。整个过程跟 Git 提交记录一样历史不可篡改错误通过新提交修复。2.3 databasechangelog 跟踪表Liquibase 自己的数据库记账本当 Liquibase 第一次执行时它会在你的业务数据库里自动建两张表DATABASECHANGELOG和DATABASECHANGELOGLOCK。这两张表是 Liquibase 的私有记账本千万别去手动改它们。DATABASECHANGELOG表记录每个成功执行的 changeSet 的 id、author、执行时间、哈希值、执行顺序等。每次启动时Liquibase 就是通过查这张表来判断哪些变更执行过、哪些没执行过。DATABASECHANGELOGLOCK表是分布式锁保证多个应用实例同时启动时只有一个实例在执行数据库迁移。有些团队生产环境是多节点部署SpringBoot 应用一启动三个实例同时连到同一个数据库如果没有这把锁三个实例会同时执行建表语句直接报表已存在。有了锁一个实例拿到锁执行完毕释放另外两个等锁释放后一看记录表发现都执行过了直接跳过。有一个经典问题是应用启动时报Could not acquire change log lock一般是有人手动删了DATABASECHANGELOGLOCK表的数据或者有应用实例在执行迁移时被强制 kill锁没释放。解决方式也很简单把DATABASECHANGELOGLOCK表里那条LOCKED1的记录手动改成 0或者直接 DELETE 掉前提是确认当前确实没有实例在跑迁移。注意如果生产环境真的出现了锁表问题先排查是不是有故障实例没死透。直接删锁要跟上线窗口配合别在业务高峰期乱动。3. SpringBoot 集成 Liquibase从起步依赖到首次启动的完整落地SpringBoot 对 Liquibase 的支持属于开箱即用级别——你只需要加依赖和配置它就能在应用启动阶段自动完成迁移。这背后是 SpringBoot 的自动装配机制在起作用LiquibaseAutoConfiguration会在你配置好spring.datasource之后自动接管。3.1 Maven 依赖和最小配置Maven 项目只需要加一个依赖dependency groupIdorg.liquibase/groupId artifactIdliquibase-core/artifactId scoperuntime/scope /dependencyGradle 的话在build.gradle里加上implementation org.liquibase:liquibase-core这里有个细节liquibase-core的版本建议跟着 SpringBoot 的 BOM 走不要自己单独指定。SpringBoot 2.7.x 里默认管理的是 Liquibase 4.x 版本SpringBoot 3.x 管理的是 Liquibase 4.20 或 4.23不同大版本的 SpringBoot 对应的默认版本兼容性不一样自己乱指定版本有时候会碰到奇怪的兼容问题。依赖加好后在application.yml里配置主 changelog 路径spring: liquibase: enabled: true change-log: classpath:/db/changelog/db.changelog-master.yaml default-schema: publicdefault-schema建议显式指定不然多数据源或不同数据库用户场景下Liquibase 建的跟踪表可能落在你预期之外的 schema 里。PostgreSQL 用户尤其要留意这个默认 public 通常没错但 MySQL 用户一般不填也行默认走连接配置里的数据库名。依赖和配置齐了之后你只需要在db/changelog/db.changelog-master.yaml里写第一个变更然后启动一次应用Liquibase 就会自动建好跟踪表并执行变更。3.2 我团队的目录组织按版本号拆分不按文件类型拆分这个目录规划我前面提到了这里详细说下为什么这样定规则。很多团队刚开始用 Liquibase喜欢把所有的createTable写在一个文件里用 include 全引进来。这在项目早期没问题但项目跑上一年你就知道有多难受了——一个 changelog 文件几千行想找一个字段的变更历史得翻半天。我建议按版本目录拆分每个目录对应一个发布版本目录里文件按序号前缀排列。比如v1.0.0/001-init-schema.sql、v1.0.0/002-init-dict-data.sql。有序号的好处是 include 的加载顺序是确定的不依赖文件名字符串排序的意外。这里顺便提一个坑Linux 环境下文件名排序是按字节码来的10-script.sql会排在2-script.sql前面。加前缀补零就是防这个。主 changelog 文件长这样databaseChangeLog: - include: file: db/changelog/v1.0.0/001-init-schema.sql - include: file: db/changelog/v1.0.0/002-init-dict-data.sql - include: file: db/changelog/v1.1.0/001-add-payment-log-table.sql - include: file: db/changelog/v1.1.0/002-add-user-coupon-index.sql还有一个小技巧如果你的团队习惯用激进重构可以在主 changelog 里用includeAll加path指定目录然后把errorIfMissingOrEmpty设成 false这样新版本目录建好了主文件不用频繁改动databaseChangeLog: - includeAll: path: db/changelog/v1.0.0/ - includeAll: path: db/changelog/v1.1.0/includeAll的排序规则是按文件路径的字符串排序所以补零规则要严格执行否则顺序会乱。我自己更倾向于显式 include因为可读性强一些但频繁加版本目录时 includeAll 确实少改一个文件看团队习惯。3.3 首次启动的完整流程与验证方法配置写好后启动 SpringBoot 应用你看日志会看到类似这样的输出2025-01-15 10:23:01.123 INFO 12345 --- [main] liquibase.database : Successfully acquired change log lock 2025-01-15 10:23:01.456 INFO 12345 --- [main] liquibase.changelog : Reading from public.DATABASECHANGELOG 2025-01-15 10:23:01.789 INFO 12345 --- [main] liquibase.changelog : ChangeSet db/changelog/v1.0.0/001-init-schema.sql::001-init-schema::zhangsan ran successfully in 42msSuccessfully acquired change log lock表示拿到锁了ChangeSet ... ran successfully表示对应变更执行成功。如果你看到ChangeSet ... has not been applied别慌那是正常跳过说明这个变更之前执行过了。启动完成之后可以验证一下。连到数据库里执行SELECT * FROM DATABASECHANGELOG ORDER BY ORDEREXECUTED;应该能看到刚才执行的变更记录。再看看你的业务表t_order应该已经建好了。如果在这步发现 Liquibase 没执行你的变更优先排查两件事第一spring.liquibase.change-log路径写没写对——路径写错它不报错只是默默不执行第二spring.liquibase.enabled是否被设成了 false——有些团队在测试快速启动场景会全局关掉它容易忘记。4. 变更集怎么写从 createTable 到复杂业务的完整案例前面配置讲完了现在进入实战核心——changeset 怎么写。很多入门教程只会带你建一个表但真实项目的变更比这复杂得多。这一章我会从简单到复杂覆盖建表、加字段加索引、初始化数据、存储过程、回滚等场景。4.1 基础 DDL建表、加字段、加索引先看一个完整的建表 changeset。用 YAML 写是 Liquibase 的推荐姿势因为结构清晰还自动支持跨数据库类型转换。比如你开发用 H2生产用 MySQL同一个 createTable 定义在两种数据库里生成的 DDL 是不同的——Liquibase 会根据连接到的数据库方言自动转换。- changeSet: id: create-payment-account-table author: lisi changes: - createTable: tableName: t_payment_account remarks: 支付账户表 columns: - column: name: id type: BIGINT autoIncrement: true remarks: 主键 constraints: primaryKey: true primaryKeyName: pk_t_payment_account nullable: false - column: name: account_no type: VARCHAR(64) remarks: 账户编号 constraints: unique: true uniqueConstraintName: uk_t_payment_account_no nullable: false - column: name: balance type: DECIMAL(18, 2) defaultValueNumeric: 0.00 remarks: 余额 - column: name: status type: TINYINT defaultValueNumeric: 1 remarks: 状态1启用 0禁用 - column: name: created_at type: DATETIME remarks: 创建时间 - column: name: updated_at type: DATETIME remarks: 更新时间一些细节值得你注意primaryKeyName和uniqueConstraintName建议显式指定。如果你不指定Liquibase 会帮你生成一个名字但不同数据库中生成的默认名规则不同后续做索引迁移或删除时找一个没有明确名称的约束会非常痛苦。再来看看常见的增量变更。项目上线后加一个字段加一个索引这是最常规的需求。示例- changeSet: id: add-merchant-id-to-payment-account author: lisi changes: - addColumn: tableName: t_payment_account columns: - column: name: merchant_id type: BIGINT remarks: 商户ID constraints: nullable: false - addNotNullConstraint: tableName: t_payment_account columnName: merchant_id columnDataType: BIGINT defaultNullValue: 0这个例子我要画个重点addColumn和addNotNullConstraint拆成了两个步骤而且addNotNullConstraint里指定了defaultNullValue: 0。为什么因为生产环境上这张表已经可能有存量数据了如果你直接新加一个NOT NULL的字段MySQL 在数据量大的情况下会重建表而且旧数据没有值直接报错。更稳妥的顺序是先加可空字段设置默认值再补上 NOT NULL 约束。这个操作顺序就是你作为有经验的人跟新手的差别所在——新手眼中都叫加字段实际却要考虑存量数据。4.2 初始化数据用 SQL 还是用 YAML有些配置类数据需要在建表后初始化比如字典表、系统参数表。Liquibase 支持在 changeSet 里直接写 insert 语句也支持直接引入 SQL 文件。- changeSet: id: init-dict-data author: lisi changes: - sql: sql: | INSERT INTO t_dict (dict_type, dict_code, dict_name, sort) VALUES (ORDER_STATUS, 1, 待支付, 1), (ORDER_STATUS, 2, 已支付, 2), (ORDER_STATUS, 3, 已取消, 3);如果你更习惯原生的 SQL 文件方式可以把整个 changeSet 直接写成一个 SQL 文件。注意文件形式的 changeSet 需要额外的头部注释来声明 id、author例如-- liquibase formatted sql -- changeset lisi:init-dict-data INSERT INTO t_dict (dict_type, dict_code, dict_name, sort) VALUES (ORDER_STATUS, 1, 待支付, 1), (ORDER_STATUS, 2, 已支付, 2), (ORDER_STATUS, 3, 已取消, 3);用 SQL 文件写有个微妙的问题它不跨数据库。YAML 里的 insert 的写法在绝大多数主流数据库里都能翻译成正确的 SQL但 SQL 文件里的方言写法一换库就没法用了。所以我个人的习惯是业务初始化数据用 YAML 的 insert 标签复杂脚本比如存储过程才用 SQL 文件而且 SQL 文件里尽量避免用方言特性。4.3 存储过程和函数Liquibase 的一个大坑存储过程是 Liquibase 使用中最容易踩坑的部分没有之一。因为存储过程里面通常有CREATE OR REPLACE PROCEDURE ... BEGIN ... END这样的结构而数据库驱动默认会按分号;切割语句。到了 BEGIN END 里面的分号它也会错误地切一刀造成语法错误。解决的办法是给 changeSet 添加两个属性splitStatements: false和endDelimiter: /。changeSet idcreate-refresh-token-procedure authorwangwu splitStatementsfalse sql endDelimiter/ CREATE OR REPLACE PROCEDURE refresh_token(IN user_id BIGINT) BEGIN DECLARE token_count INT; SELECT COUNT(*) INTO token_count FROM t_auth_token WHERE uid user_id; IF token_count 0 THEN UPDATE t_auth_token SET refresh_at NOW() WHERE uid user_id; END IF; END; / /sql /changeSetsplitStatements: false告诉 Liquibase 不要把这段 SQL 切片整段作为一个语句执行endDelimiter指定这段脚本的结束符。这个细节我看十个人有九个人会踩而且报错信息往往是通用语法错误很迷惑。如果你团队里有人负责写存储过程把这篇文章转给他看看能少废几个晚上。4.4 回滚策略不能只会上不敢下Liquibase 有个很强的能力是支持回滚。每个 changeSet 可以显式定义一个 rollback 操作。不加 rollback 定义的话Liquibase 会自动生成默认回滚语句——对于 createTable 是 dropTable对于 addColumn 是 dropColumn。但是对于 SQL 文件或者复杂变更自动回滚常常不生效建议显式写。YAML 里这样写- changeSet: id: add-merchant-id-to-payment-account author: lisi changes: - addColumn: tableName: t_payment_account columns: - column: name: merchant_id type: BIGINT constraints: nullable: false rollback: - dropColumn: tableName: t_payment_account columnName: merchant_id然后你可以通过 Maven 插件或命令行执行回滚到指定 tag 或 datemvn liquibase:rollback -Dliquibase.rollbackCount1要注意的是回滚在生产环境并不是你想的那样随意执行的。生产环境回滚数据库结构本身风险就高别指望所有变更都能靠 Liquibase 一键回滚。Liquibase 的回滚更常用于开发环境重置和测试环境重建生产环境的回滚应该配合数据备份。这个认知要建立起来把它当银弹是不行的。5. 多环境管理与团队协作中的配置策略Liquibase 用起来之后你很快会遇到几个团队级的难题不同环境怎么管理同一套 changelog多人同时改 changelog 冲突了怎么办怎么配合 CI/CD 在发布流水线里自动执行这一章专门讲这些实操经验。5.1 多环境配置不要再维护三份 changelog团队最容易犯的错是给开发、测试、生产分别维护不同的 changelog 文件。这种做法的结果是三个月后三个环境的变更记录就对不上了又回到最初的手工维护混乱状态。Liquibase 的理念是一份 changelog多个环境共用。环境差异通过参数注入和 context 来解决。什么是 context简单说就是给 changeSet 打一个标签只在特定环境下执行。在 yml 里配置 context 也会有专门属性。看一个实际例子。假设你的生产环境要求所有表都有注释而开发环境为了启动速度不想跑这些注释变更。你可以在主 changelog 里定义两个 context 的 changeSet- changeSet: id: add-table-comments author: zhangsan context: dev,test changes: - sql: sql: COMMENT ON TABLE t_order IS 订单表然后在application-prod.yml里spring: liquibase: contexts: prod在application-dev.yml里spring: liquibase: contexts: dev这样同一个 changelog 文件在不同环境执行时会自动过滤掉不该执行的 changeSet。我们团队目前的规范是通用 schema 变更不加 context初始化数据加dev,testcontext生产环境的初始化数据另外用脚本导入避免生产环境被测试数据污染。另外就是spring.profiles.active的变化会影响 Liquibase 里的 context 配置所以使用 SpringBoot 多配置的同学要特别注意contexts和 active profile 不是一回事Liquibase 只认spring.liquibase.contexts里的值。5.2 多人协作时的冲突避免方案changelog 本质是文件多人改文件就会产生 Git 冲突。尤其是大家都追加变更到同一个 changelog 目录时冲突概率很高。我们的做法是规定一个独立需求或任务对应一个独立的 changelog 文件文件名包含需求编号。比如v1.2.0/20250115-JIRA-1024-add-refund-table.sql。大家各写各的文件只在合并到主分支时才通过主 changelog 的 include 统一引入。这样冲突基本不会发生code review 的时候还能直接按需求编号找到变更文件。还有一点值得提不要在 master 分支上直接改主 changelog 的 include 顺序。有些同学本地开发时会随手在 include 列表里加一行自己的文件结果跟别人合并时到处是冲突。我们现在的规范是主 changelog 只在发布分支上由版本负责人修改。多人合并时先合代码后加 include顺序不能颠倒。5.3 CI/CD 集成让变更跟着应用一起发布Liquibase 和 SpringBoot 的整合本身已经覆盖了应用启动时自动迁移的场景但这还不够。在 CI/CD 流水线里你通常需要做两件事一是执行变更到目标环境二是验证变更结果。如果你的流水线是mvn package然后docker build然后部署那么 SpringBoot 应用在容器启动时会自动跑 Liquibase这是最省心的方式。但有些项目团队倾向更早发现 SQL 问题不希望等容器启动到一半才报错。这时可以在 CI 里单独加一步mvn liquibase:update -Dliquibase.urljdbc:postgresql://staging-db:5432/app -Dliquibase.usernameapp -Dliquibase.passwordxxx我的建议是SQL 语法错误应该在测试环境跑集成测试时就能暴露而不要在部署阶段才暴露。所以 CI 的测试阶段就应该连测试库跑一次mvn liquibase:update或者依赖 SpringBoot 的启动测试。把数据库变更验证前置到测试阶段能极大减少线上部署的意外。此外如果你用 Docker 部署记得在镜像构建时把db/changelog目录打进 jar 包里。这个目录在 resources 下默认会被包含但如果你在 gradle 里做了processResources的自定义过滤有可能把它排掉了。我带队时真遇到过镜像里没有 changelog 文件的情况应用启动日志显示Unable to read classpath:/db/changelog/db.changelog-master.yaml排查还花了不少时间。检查一下你构建产物的BOOT-INF/classes/db/changelog/目录是否存在最稳妥。5.4 环境差异的特殊处理MySQL vs PostgreSQL vs H2Liquibase 的一个重要卖点是跨数据库方言适配。开发环境用 H2 做测试生产环境用 MySQL理论上同一份 changelog 可以跑通。但现实里总有意想不到的差异。举个例子MySQL 的DATETIME默认值不能用函数比如DEFAULT CURRENT_TIMESTAMP在某些版本下的表现不同PostgreSQL 的BOOLEAN类型在 MySQL 里没有H2 对很多 MySQL 特有语法支持有限。这些方言差异在你写 YAML 类型的 changeset 时大多能被 Liquibase 自动处理但一旦你用了原生 SQL 文件就得格外谨慎。一个稳妥的原则能用 YAML 类型 changeSet 实现的操作就不要用 SQL 文件必须在 SQL 文件里写复杂逻辑时尽量保持 ANSI SQL 兼容。项目如果同时支持 MySQL 和 PostgreSQL建议搭建一个多数据库的集成测试任务把 changelog 分别在两种数据库中跑一遍这是最可靠的验证手段任何我觉得应该没问题的判断都不如实际跑一次。6. 踩过的坑从 checksum 失配到数据库锁死附排查链路接下来这一部分是我最想写的内容也是别人博客里很少系统讲到的。Liquibase 本身没有太多复杂的代码技巧真正的学习成本全在踩坑后的排查过程里。我把这些年遇过的典型问题按症状 - 排查链路 - 根因 - 修复列出来方便大家以后对照。6.1 改了一个已执行的 changeSet启动直接失败这是最常见的坑。现象是应用启动报错LiquibaseException: Validation failed: 1 change sets check sum mismatch排查链路很简单报错会明确告诉你哪个文件、哪个 changeSet 校验失败。先去DATABASECHANGELOG表里查这个 changeSet 的MD5SUM值再把当前 changelog 文件里内容跑一下liquibase.calculateCheckSum你会发现两个 MD5 确实不同。根因只有一个——有人改了已执行的 changeSet 内容。我见过很多种改法觉得注释写错了直接改注释的发现字段长度短了直接改 type 的甚至有人把 changeset 顺序调换导致同一个 id 关联的内容变了。这些全都触发校验失败。修复方式按正确程度排序首选且唯一推荐不要改旧 changeSet新写一个 changeSet 来做修正。如果改动发生在开发早期、还没提交合并可以mvn liquibase:clearCheckSums然后让 Liquibase 重新计算。这招慎用只适合开发环境千万别在生产环境乱来。最差的情况是改动已经上了生产但你又必须修正历史脚本——这时候只能找 DBA 手动改DATABASECHANGELOG表的MD5SUM记录。这种做法我不推荐相当于告诉数据库这个脚本改过但你别管本质上是绕过版本管理的约束风险自负。经验就是执行过的 changeSet 就是历史改历史这种念头趁早打消。你越早建立这个意识后面痛得越少。6.2 数据库锁死Could not acquire change log lock第二个高频问题。多实例部署时一个实例正在跑迁移另一个实例启动拿不到锁日志长这样Waiting for changelog lock.... Waiting for changelog lock.... ... Could not acquire change log lock. Currently locked by Host:xxx这种情况下你去看DATABASECHANGELOGLOCK表大概率是LOCKED1。造成锁不释放的原因通常是执行迁移的应用实例被强杀比如 Kubernetes 滚动更新时 OOMKilled、或者迁移执行时间过长连接中断、或者有人手动执行了 Liquibase 命令后中断。处理步骤先确认当前环境是不是真的没有实例在跑迁移。如果是多副本部署看一遍所有 Pod 状态确保没有正在初始化的实例。连到数据库查看DATABASECHANGELOGLOCKSELECT * FROM DATABASECHANGELOGLOCK;确认可以安全释放后DELETE FROM DATABASECHANGELOGLOCK WHERE ID 1;Liquibase 下次启动会重新创建锁记录。这里有个细节尽量别只把LOCKED改成 0直接 DELETE 更干净避免残留的锁拥有者信息干扰判断。生产环境遇到这种情况要克制住先删了再说的冲动。锁存在的意义是防止并发执行你在不确定其他实例是否在跑的情况下删锁可能引发两个实例同时执行 DDL报错会更难看。所以流程必须是先确认所有应用实例状态再删锁然后重启失败的那个实例或者让它自动重试。6.3 跟 Hibernate ddl-auto 的冲突这个问题在 SpringBoot JPA 项目中很典型。有些团队引入了 Liquibase但spring.jpa.hibernate.ddl-auto还保留着update甚至create-drop。结果就是Liquibase 刚建好表Hibernate 也去修改表结构两边打架或者 Hibernate 直接把表建好了Liquibase 再跑时发现表已存在报table already exists。正确的做法是二选一全部用 Liquibase 管理设spring.jpa.hibernate.ddl-auto: validate。Hibernate 启动时校验实体和表结构是否匹配不匹配就报错非常安全。开发环境图省事用 Hibernate 自动建表测试和生产环境用 Liquibase。但这种方式要求团队严格执行很容易出现开发环境跑得通测试环境一启动就报错的情况。我的建议还是生产意识从开发就建立统一用 LiquibaseHibernate 设 validate。刚开始会不习惯但时间长了你会发现数据库结构变更的开发体验反而更顺畅了——所有的结构定义都在 changelog 里你不需要去翻实体类猜字段。6.4 SQL 文件里的分号陷阱前面讲存储过程时我提过 splitStatements 的问题这里再说细一点。Liquibase 默认对 SQL 文件按分号切割这个规则对普通 DDL 没问题但对以下场景会出幺蛾子存储过程/函数体内部的分号触发器内部的分号MySQL 的 delimiter 命令包含分号的注释直接给结论凡是复杂 SQL 对象都建议用 XML/YAML 的sql标签包起来并设置splitStatements: false或者在 SQL 文件头部加-- liquibase formatted sql并利用 SQL 文件格式里的语法。最省心的做法就是把数据库特定对象存储过程、函数、触发器统一放到一个db/procedures/目录里用 XML changeSet 引用避免跟普通表结构混在一起。这样可以隔离风险排查也简单。6.5 大小写引号问题一位老哥的 PostgreSQL 迁移血泪史PostgreSQL 对大小写敏感而且未加引号的标识符会默认转成小写。有一次我们团队在 YAML changeSet 里定义了一个表tableName: OrderInfo结果 Liquibase 在 PostgreSQL 上生成的 SQL 是CREATE TABLE OrderInfo (...)——Liquibase 默认没有给标识符加双引号PostgreSQL 把它转成了小写orderinfo而我们的代码里用的是order_info于是启动时 JPA 校验直接失败。解决办法是统一用下划线小写命名并保持一致性表名、字段名全部小写下划线这样无论 MySQL 还是 PostgreSQL 都能稳定工作。如果确实有大写历史遗留可以在 changeSet 上指定schemaName和catalogName但尽量压住不要用。数据库命名规范这事跟那个 SQL Server 系的同事砍了八百回我这边底线就只有一条跨数据库兼容的 changelog 必须小写下划线。6.6 并行执行 DDL 导致死锁在 MySQL InnoDB 下DDL 语句是排他锁。两个 changeSet 连续对同一张表做修改时在高并发多实例下可能出现锁等待超时。这个问题的根因其实是 changelog 设计的原子性不足——你在一个 changeSet 里做了一堆操作或者两个 changeSet 隐式依赖同一个表的高频变更。预防比修复重要尽量一个 changeSet 只做一件事减少锁的范围。不要在一个 changeSet 里同时对同一张表 drop column 和 modify column拆成两个独立 changeSet。如果变更数据量巨大比如给千万级表加索引可以配置changeSet的runInTransaction: false配合 MySQL 的在线 DDL。runInTransaction: false是一个被低估的属性。默认 Liquibase 会把每个 changeSet 包在事务里但 MySQL 下 DDL 不支持事务回滚强行包事务反而容易造成隐式提交的诡异行为。数据量大的表加索引该关事务就关。7. 写在最后用了一年 Liquibase 后的真实体会如果只看官方文档Liquibase 看起来就是一个把 SQL 写成脚本然后自动执行的工具但用久了你会发现它的价值不在于自动化本身而在于它把数据库结构变成了一件可以被审查、被追踪、被回滚的普通代码。我自己最深的体会是引入 Liquibase 对团队纪律的要求远高于对技术能力的要求。它默认给的自由度很大——你可以任意改历史脚本、你可以用不在版本控制里的方式动数据库、你可以把所有变更塞进一个巨大无比的 changelog 文件系统都不会拦你代价是未来某天的一次线上事故。所以团队要做的不是装个工具就完事而是把目录规范、命名规范、code review 规范、上线前验证流程一并定下来Liquibase 才能发挥它真正的威力。最后再分享一个非常实用的小技巧。如果你在开发过程中频繁改库表结构本地数据库里积累了一堆已经执行过的 changeSet你想回到一个干净的状态重新开始别去手动删表。Liquibase 提供了dropAll命令mvn liquibase:dropAll或者直接删掉本地开发库重建一个空库然后启动应用让 changelog 重新跑一遍。这个操作只建议在开发环境用但确实能让你从数据库结构改乱了又不知道从哪开始的困境里快速脱身。反正我开发阶段这么干过无数次每次都觉得当初把数据库结构交给版本管理这个决定做得太值了。
返回列表