Liquibase:把数据库变更变成可验证、可回退的交付链
凌晨发布窗口里,两个应用实例同时启动。第一个实例取得数据库变更锁,执行到一条大表 ALTER TABLE 时被平台超时终止;第二个实例一直等待。值班人员看到 DATABASECHANGELOGLOCK.LOCKED=true,直接执行 release-locks,随后重新发布。新进程绕过了仍在数据库中运行的 DDL,又继续执行后续 changeSet。最终,应用代码已经按“新列必然存在”运行,部分节点却面对旧结构;一名开发者为消除 checksum 报错,又修改了已经执行过的 changeSet,并运行 clear-checksums。事故从一次数据库超时,扩大成了执行者、数据库真实结构和变更历史三方失真。
Liquibase 的价值不只是“按顺序执行 SQL”。它把期望变更写进 changelog,以 changeSet 标识不可变的变更单元,用 DATABASECHANGELOG 记录已执行事实,用 DATABASECHANGELOGLOCK 串行化执行者,再通过 precondition、context、label、SQL 预览与 rollback 把风险尽量提前暴露。工具能建立证据链,却不会替团队判断 DDL 是否在线安全、数据回填能否逆转,也不会替代备份恢复和数据库审查。
先认清四个状态,才不会把“命令成功”当成数据库正确
一次 update 会同时接触四类状态:
Git 中的 root changelog 与被 include 的 changeSet,描述团队期望执行什么。目标数据库的 schema 与业务数据,代表此刻真实状态。DATABASECHANGELOG 中的执行记录,保存 changeSet 身份、校验和、执行顺序、部署标识和执行类型。
DATABASECHANGELOGLOCK 中的互斥状态,保证同一组跟踪表只允许一个 Liquibase 执行者推进。
Liquibase 读取 changelog 后,以 id、author 和 changelog 文件路径组成 changeSet 身份,再与 DATABASECHANGELOG 比较。未出现的身份进入待执行集合;已经出现的普通 changeSet 不再执行,但会重新计算 checksum,用来发现执行后被修改的内容。官方的 DATABASECHANGELOG 表说明列出了 FILENAME、MD5SUM、ORDEREXECUTED、EXECTYPE 和 DEPLOYMENT_ID 等关键字段。
这也解释了三类常见误判:数据库里“已经有这张表”不等于 changeSet 已记录;记录存在不等于业务数据已经正确迁移;锁仍存在不等于一定可以释放。诊断必须同时检查 changelog 身份、跟踪表和数据库真实对象。
安装时固定发行线、驱动和制品来源
Liquibase Community 提供 CLI、容器、Java 库和主流数据库迁移能力;Liquibase Secure 增加策略检查、特定回滚命令和商业治理能力,需要相应许可。从 5.0 起,两条发行线已拆成独立分发:传统 GitHub、Maven 与 liquibase/liquibase 渠道提供 Community,Secure 使用单独下载、依赖坐标与 liquibase/liquibase-secure 镜像,Community 制品不能靠注入 Secure license key 解锁商业能力。Community 5.x 采用 Functional Source License,Secure 使用商业 EULA;企业采用前要由法务或开源治理流程核对目标版本和使用方式,不把“可免费下载”等同于“没有使用边界”。当前 Community 稳定版本与升级说明应从 Liquibase Community 发布说明核对,CI 使用批准版本或镜像 digest,不能追随 latest。
本机安装后先验证 Java、Liquibase、驱动和实际路径:
java -version
liquibase --version
command -v liquibase
liquibase --helpmacOS 可以使用组织批准的 Homebrew 源,Linux 与 Windows 可以使用官方安装包或固定版本压缩包。Community 5.x 的分发包不再默认携带扩展、驱动和许多附加包,优先用内置的 liquibase lpm 查询并安装批准依赖;离线或受控环境也可以把经过摘要校验的 JDBC driver 放入 $LIQUIBASE_HOME/lib 或显式 classpath。两种方式都要把 Liquibase、扩展和驱动作为一组制品锁定,不能只固定 CLI 版本。Secure 自带依赖且不使用 LPM,不能把 Community 的依赖安装步骤原样套用。容器方式更容易固定运行时:
liquibase lpm list
liquibase lpm add postgresql
liquibase --versionexport LIQUIBASE_IMAGE='liquibase/liquibase:<APPROVED_COMMUNITY_VERSION>'
docker pull "$LIQUIBASE_IMAGE"
docker image inspect "$LIQUIBASE_IMAGE" --format '{{index .RepoDigests 0}}'
docker run --rm "$LIQUIBASE_IMAGE" --version镜像只提供执行器,数据库仍必须能从容器网络访问。开发库在宿主机时,Windows 与 macOS 可评估 host.docker.internal;Linux 应使用明确的 Compose 网络、host-gateway 或数据库容器服务名。不要为了连通而把数据库端口开放到公共网卡。
CLI 支持 defaults file。下面只保存非敏感默认值,用户名和密码由进程环境、CI secret 或工作负载身份注入:
# liquibase.properties
changelogFile=db/changelog/root.yaml
url=jdbc:postgresql://db.example.invalid:5432/catalog
logLevel=info执行时用 LIQUIBASE_COMMAND_USERNAME、LIQUIBASE_COMMAND_PASSWORD 直接覆盖连接身份,不必在 properties 中再写一层 ${...} 占位。排查“properties 明明改了却没有生效”时,要按官方的 configuration hierarchy 查找覆盖源:Flow stage 参数高于 Flow 全局参数,随后是命令行、Servlet 初始化参数、Java system properties、操作系统环境变量,defaults file、标准输入和 connection profile 等配置数据位于更低层。生产发布器应限制动态 CLI 拼接和 JAVA_OPTS,并在执行前输出脱敏后的目标、changelog、context 与 label 摘要。
不要把密码拼进 JDBC URL、命令行或仓库里的 properties。命令行参数可能进入进程列表和流水线日志;properties 也不是密码库。环境变量仍可能被同权限进程、容器 inspect 或错误的调试日志读取,生产环境应优先使用受保护 runner、短期凭据或工作负载身份。数据库账号应只拥有目标 schema 所需 DDL/DML 权限,不应持有超级用户、复制、操作系统文件或跨业务 schema 权限。
从 root changelog 建立稳定的变更身份
一个可维护的项目把 root changelog 作为有序索引,显式 include 每个变更文件:
db/
changelog/
root.yaml
changes/
catalog-001-create-product.yaml
catalog-002-add-status.yaml# db/changelog/root.yaml
databaseChangeLog:
- include:
file: changes/catalog-001-create-product.yaml
relativeToChangelogFile: true
- include:
file: changes/catalog-002-add-status.yaml
relativeToChangelogFile: trueLiquibase 按 include 出现的顺序处理文件。relativeToChangelogFile: true 让路径相对 root 文件解析,避免本机、容器和 CI 的工作目录差异。includeAll 会按资源比较器决定的顺序加载目录内容,适合规则高度稳定的生成目录;核心业务变更更适合显式 include,让顺序变化直接出现在代码评审中。官方 include 说明还提醒:重复 include 不会主动报错,循环 include 也不会被自动识别,因此 root 文件必须接受静态检查。
第一个 changeSet 使用 YAML 建模,显式给出回滚:
# db/changelog/changes/catalog-001-create-product.yaml
databaseChangeLog:
- changeSet:
id: catalog-001-create-product
author: platform-team
labels: catalog-core
changes:
- createTable:
tableName: product
columns:
- column:
name: id
type: bigint
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: varchar(120)
constraints:
nullable: false
rollback:
- dropTable:
tableName: productid 在同一 author 和路径下唯一即可,但团队应采用稳定、可搜索且不依赖时间戳碰运气的命名。文件一旦在任一共享环境执行,就把 id + author + 路径 + 内容 当作不可变制品。修复错误要追加新的 changeSet;移动已执行文件会改变身份,确需重构目录时应先评估 logicalFilePath,并在所有环境验证跟踪记录匹配,不能把文件搬家当作普通整理。
用 PostgreSQL 临时库跑通正向链路
下面的实验使用合成数据和一次性 PostgreSQL 容器。把上述三个文件放入当前目录的 db/changelog 后执行:
set -eu
NET='liquibase-lab-net'
DB='liquibase-lab-db'
docker network create "$NET"
docker run -d --rm --name "$DB" --network "$NET" \
-e POSTGRES_DB=catalog \
-e POSTGRES_USER=lb_runner \
-e POSTGRES_PASSWORD='synthetic-lab-only' \
postgres:15-alpine
until docker exec "$DB" pg_isready -U lb_runner -d catalog; do sleep 1; done
docker run --rm --network "$NET" \
-v "$PWD:/liquibase/work" -w /liquibase/work \
-e LIQUIBASE_COMMAND_PASSWORD='synthetic-lab-only' \
"$LIQUIBASE_IMAGE" \
--url='jdbc:postgresql://liquibase-lab-db:5432/catalog' \
--username=lb_runner \
--changelog-file=db/changelog/root.yaml validate
docker run --rm --network "$NET" \
-v "$PWD:/liquibase/work" -w /liquibase/work \
-e LIQUIBASE_COMMAND_PASSWORD='synthetic-lab-only' \
"$LIQUIBASE_IMAGE" \
--url='jdbc:postgresql://liquibase-lab-db:5432/catalog' \
--username=lb_runner \
--changelog-file=db/changelog/root.yaml updatevalidate 通过只证明 changelog 能被解析并满足静态约束,不证明 SQL 在目标数据量上安全。update 成功后,直接查询三类证据:
docker exec "$DB" psql -U lb_runner -d catalog -v ON_ERROR_STOP=1 -c \
"select column_name,data_type from information_schema.columns where table_name='product' order by ordinal_position;"
docker exec "$DB" psql -U lb_runner -d catalog -v ON_ERROR_STOP=1 -c \
'select id,author,filename,md5sum,exectype,orderexecuted from databasechangelog order by orderexecuted;'
docker exec "$DB" psql -U lb_runner -d catalog -v ON_ERROR_STOP=1 -c \
'select id,locked,lockgranted,lockedby from databasechangeloglock;'预期 product 包含 id 与 name,执行记录出现 catalog-001-create-product,锁表回到 locked=false。再次运行 update 应显示没有新的 changeSet,执行记录数量保持不变;这才是最小幂等证据。
DATABASECHANGELOG 是历史账本,不是真实结构的替身
DATABASECHANGELOG 没有用数据库约束强制唯一键,但 Liquibase 逻辑上以 changeSet 身份判断是否执行。ORDEREXECUTED 只保证单次更新中的顺序,不应被当作跨数据库全局序列;多环境比较要使用身份、checksum、标签、部署记录和真实结构,而不是只比较最大序号。
修改已经执行的 changeSet 后再运行 update,会触发 checksum 校验失败。这是应保留的反向实验:
# 在隔离分支把 name 的 varchar(120) 临时改为 varchar(160),不要提交修改。
set +e
docker run --rm --network "$NET" \
-v "$PWD:/liquibase/work" -w /liquibase/work \
-e LIQUIBASE_COMMAND_PASSWORD='synthetic-lab-only' \
"$LIQUIBASE_IMAGE" \
--url='jdbc:postgresql://liquibase-lab-db:5432/catalog' \
--username=lb_runner \
--changelog-file=db/changelog/root.yaml update >checksum.out 2>&1
rc=$?
set -e
test "$rc" -ne 0
grep -Ei 'checksum|validation failed' checksum.out正确修复是恢复原 changeSet,再追加 catalog-003-widen-product-name。Liquibase 的 checksum 针对 changeSet 的序列化内容,不是整个 changelog 文件的字节哈希;普通空白或注释整理未必改变它。MD5SUM 中冒号前的数字还是算法版本,工具升级可能在不改变 changeSet 的情况下静默升级并重算 checksum,不能把这种受支持的算法迁移误判为历史篡改。真正的内容校验失败仍应先比对已发布制品、当前 changelog、Liquibase 版本和数据库记录。
validCheckSum 只适合经过审查、确认数据库现状与两份内容语义等价的兼容场景;clear-checksums 会把 DATABASECHANGELOG.MD5SUM 的全部现有校验和置空。下一次 update 不只为已执行 changeSet 重新计算 checksum,还会照常部署所有待执行 changeSet,因此不能把它当作只读校验修复。官方 changeset checksum 说明明确区分了内容变化与算法版本升级。没有变更工单、环境比对、待执行集合审查、备份和双人复核,不应在共享环境执行 clear-checksums。
也不要手工插入、删除或修改 DATABASECHANGELOG 来“对齐”。那会把工具记录变成愿望清单,却不会创建、删除或恢复任何真实对象。需要接管既有数据库时,应使用受控 baseline 或 changelog-sync-sql 先审查将写入的标记,再执行同步,并为每个环境保留接管时的 schema 快照和批准记录。
锁只保护 Liquibase 执行者,不保护所有 DDL
DATABASECHANGELOGLOCK 通常只有一行。执行者先把 LOCKED 从 false 更新为 true,成功取得锁后才推进 changeSet;其他 Liquibase 进程等待。它不能阻止 DBA、应用初始化代码或另一个迁移工具直接执行 DDL,也不能把非事务 DDL 自动变成原子操作。
遇到锁长时间未释放,按下面顺序判断:
查询 LOCKEDBY 与 LOCKGRANTED,定位执行节点和取得时间。检查对应 CI job、Pod、应用实例或 CLI 进程是否仍存活。查询数据库活动会话、阻塞链、当前 SQL 和事务状态,确认 DDL 是否仍在运行。
核对 DATABASECHANGELOG 最后一条记录与数据库真实结构,判断 changeSet 停在执行前、执行中还是执行后未记账。只有确认原执行者和 SQL 均已终止,且现场证据已保存,才运行 release-locks。
liquibase \
--url="$LIQUIBASE_COMMAND_URL" \
--username="$LIQUIBASE_COMMAND_USERNAME" \
--changelog-file=db/changelog/root.yaml list-locks
# 完成人工与数据库会话确认后才允许执行。
liquibase \
--url="$LIQUIBASE_COMMAND_URL" \
--username="$LIQUIBASE_COMMAND_USERNAME" \
--changelog-file=db/changelog/root.yaml release-locks如果数据库已提交 DDL、但 Liquibase 尚未写入执行记录,直接释放锁并重跑可能再次执行同一操作;如果 DDL 仍在后台执行,释放锁会制造并发变更。锁恢复操作必须和数据库会话证据、对象检查以及后续补偿 changeSet 一起审批。
precondition 应拒绝错误状态,而不是粉饰历史
第二个 changeSet 在增加 status 前检查表存在,并拒绝非 PostgreSQL 目标:
# db/changelog/changes/catalog-002-add-status.yaml
databaseChangeLog:
- changeSet:
id: catalog-002-add-status
author: platform-team
context: "@schema"
labels: catalog-core
preConditions:
- onFail: HALT
- onError: HALT
- dbms:
type: postgresql
- tableExists:
tableName: product
changes:
- addColumn:
tableName: product
columns:
- column:
name: status
type: varchar(24)
defaultValue: active
constraints:
nullable: false
rollback:
- dropColumn:
tableName: product
columnName: statusonFail 表示条件被正常计算但结果不满足,onError 表示检查本身出错。HALT 让不确定状态停下来;WARN 会继续执行;changeset 级 CONTINUE 会跳过且下次再尝试;MARK_RAN 不执行变更却写入已运行记录。对创建关键结构、权限和数据修复,MARK_RAN 往往会制造账实不符,应只用于有明确业务语义、可证明“目标状态已经由其他路径完成”的场景。
部分 tableExists、indexExists 等检查可能生成数据库 snapshot,在大 schema 或高延迟连接上带来时间和元数据锁成本。高频、关键检查可以改用数据库专用 sqlCheck,但 SQL 需要按数据库版本验证。官方 preconditions 说明给出了各动作语义;precondition 是运行时护栏,不是替代变更审查的万能断言。
context 与 label 是过滤器,不是安全边界
context 适合描述运行环境或执行通道,label 适合描述功能、发布组或风险分类。两者都支持逻辑表达式,但最危险的默认行为是:运行命令没有传过滤器时,带普通 context 或 label 的未执行 changeSet 仍可能运行。两类 @ 的位置和语义不同:示例中的 context: "@schema" 写在 changeSet 上,要求调用方显式提供匹配的 context filter;label 的 @ 写在运行时 --label-filter='@catalog-core' 中,表示只选中带该 label 的 changeSet,连无 label 的 changeSet 也排除。CI 既要拒绝空参数,也要验证严格 label 表达式没有被普通 catalog-core 覆盖。
liquibase \
--changelog-file=db/changelog/root.yaml \
--context-filter='schema' \
--label-filter='@catalog-core' \
status --verbose
liquibase \
--changelog-file=db/changelog/root.yaml \
--context-filter='schema' \
--label-filter='@catalog-core' \
update-sql --output-file=artifacts/update.sql官方 contexts 与 labels文档说明了无过滤器时的行为。CI 应对“参数为空”直接失败,并把 context、label、目标数据库标识和生成 SQL 一起归档。过滤器不能代替数据库账号隔离:即使误选 changeSet,目标账号也应因最小权限而无法越过允许的 schema。
update-sql 先展示计划,rollback-sql 先证明退路
update-sql 生成 Liquibase 计划执行的 SQL,不修改目标业务结构;它仍依赖目标数据库当前跟踪历史、数据库类型、properties、参数和过滤器。生成 SQL 的环境与实际执行环境不一致时,预览没有证明力。
liquibase \
--url="$LIQUIBASE_COMMAND_URL" \
--username="$LIQUIBASE_COMMAND_USERNAME" \
--changelog-file=db/changelog/root.yaml \
--context-filter='schema' \
--label-filter='@catalog-core' \
update-sql --output-file=artifacts/update.sql
liquibase \
--url="$LIQUIBASE_COMMAND_URL" \
--username="$LIQUIBASE_COMMAND_USERNAME" \
--changelog-file=db/changelog/root.yaml \
rollback-sql --tag=before_catalog_release \
--output-file=artifacts/rollback.sql审查生成 SQL 时关注锁类型、全表扫描、表重写、默认值回填、索引构建方式、语句超时、隐式提交、对象限定名、权限变化和数据删除。update-sql 不会模拟真实数据量、并发流量或数据库版本差异,所以关键 DDL 还要在生产等价的影子库上执行并采集耗时、锁等待、WAL/binlog 增量、磁盘峰值和复制延迟。
回滚以 tag、count 或其他受支持定位方式选择已执行 changeSet,并按逆序执行 rollback。tag 必须在发布前的已知稳定执行点真实存在,不能在故障后临时给当前状态打同名 tag。稳定点完成验证后先记录:
liquibase \
--url="$LIQUIBASE_COMMAND_URL" \
--username="$LIQUIBASE_COMMAND_USERNAME" \
--changelog-file=db/changelog/root.yaml \
tag --tag=before_catalog_release随后发布的新 changeSet 才能使用前面的 rollback-sql --tag=before_catalog_release 预览逆向计划。能自动反向生成的 change type 也不代表数据可恢复:dropColumn 的逆操作可以重建列,却不能恢复被删除的数据;重命名、拆表、编码转换和回填需要显式 rollback 或 roll-forward 补偿。执行 rollback 前先运行对应的 rollback-sql,再在隔离副本演练。按 tag、日期或 count 的标准 rollback 及对应 SQL 预览可用于 Community;任意挑选一条 changeSet 或按 deployment ID 回退的 rollback-one-changeset、rollback-one-update 及其预览命令属于 Secure。许可不同不会改变依赖顺序风险,定点回退仍可能破坏后续 changeSet 的前提。
失败恢复先判断事务边界,再决定重试还是补偿
Liquibase 默认按 changeSet 组织事务,但能否原子回滚取决于数据库和具体 DDL。PostgreSQL 的许多 DDL 可进入事务,MySQL 的许多 DDL 会隐式提交,在线索引、存储过程、外部脚本和 runInTransaction: false 又有各自边界。把 runInTransaction 关掉后,如果同一 changeSet 有多条语句且中途失败,前半段可能已经提交而 DATABASECHANGELOG 没有成功记录,下一次自动重试会再次撞上半成品。一个 changeSet 塞入多条高风险语句,会让中间状态难以判断;团队应把能够独立验证和补偿的动作拆成小 changeSet,同时避免把彼此必须原子完成的约束盲目拆散。
Secure 的 rollback-on-error 只尝试回退当前 update 已部署的 changeSet,不是数据库级“全局事务”。若失败 changeSet 含多个 change 且已经部分落地,是否把它纳入回退还受 force-on-partial-changes 控制;failOnError=false 又会让该错误不触发自动回退。即使开启相关参数,目标数据库的隐式提交和 rollback 定义仍决定最终可恢复范围。启用前必须先用相同发行版、参数和数据库类型演练失败点,不能把一个商业开关写成跨隐式提交 DDL 的原子性承诺。
失败后按证据分型:
没取得锁:没有业务变更发生,排查竞争执行者和流水线编排。已取得锁但 precondition 失败:检查条件与目标环境,不要改成 WARN 强行通过。SQL 执行失败且事务已回滚:确认真实对象未变化,再修复为新的 changeSet 或在未共享执行前修订。
DDL 已部分提交但执行记录缺失:冻结自动重试,检查对象、数据、数据库日志和会话,设计补偿 changeSet。SQL 成功但应用不兼容:优先使用向前兼容的 expand/contract,必要时按已演练 rollback 和备份恢复方案处置。checksum 失败:恢复不可变历史并追加修复,禁止用清校验和掩盖来源不明的差异。
runOnChange 适合视图、存储过程等“定义全文变化就重新应用”的对象;runAlways 每次更新都执行。两者会改变普通 changeSet 的不可变语义,不能用于高风险 DDL、一次性数据修复或审计不可重复动作。可重复对象仍要保证幂等、权限可控,并用数据库对象定义比对验证结果。
Spring Boot 接入要避免每个副本都成为迁移执行者
Spring Boot 加入 liquibase-core 后,可在应用启动阶段运行迁移:
spring:
liquibase:
enabled: true
change-log: classpath:/db/changelog/root.yaml
contexts: schema
label-filter: "@catalog-core"
default-schema: catalog_app启动集成适合本地开发和单实例环境,能让代码与 schema 一起验证;在多副本生产发布中,所有实例同时启动会竞争锁,迁移耗时还会占用 readiness 窗口。更稳健的做法是把 Liquibase 作为一次性部署 Job,由独立迁移账号执行,成功后再放行业务实例。应用运行账号只保留 DML 权限,迁移账号按审批临时取得目标 schema 的 DDL 权限。
应用启动日志不得打印完整 JDBC URL、密码或生成 SQL 中的敏感数据。spring.liquibase.drop-first 会在迁移前删除对象,不能进入共享环境配置;enabled=false 也不应成为绕过失败迁移的临时开关。关闭应用内迁移时,流水线必须有唯一、可审计的替代执行者。
容器与 CI 把同一份 changelog 变成受控制品
Compose 可以让本地数据库和一次性迁移服务共享网络:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: catalog
POSTGRES_USER: lb_runner
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lb_runner -d catalog"]
interval: 2s
timeout: 2s
retries: 30
secrets: [db_password]
migrate:
image: liquibase/liquibase:<APPROVED_COMMUNITY_VERSION>
depends_on:
db:
condition: service_healthy
volumes:
- ./db/changelog:/liquibase/changelog:ro
entrypoint: ["/bin/sh", "-ec"]
command: >-
export LIQUIBASE_COMMAND_PASSWORD="$$(cat /run/secrets/db_password)";
exec liquibase
--url=jdbc:postgresql://db:5432/catalog
--username=lb_runner
--changelog-file=/liquibase/changelog/root.yaml
update
secrets: [db_password]
secrets:
db_password:
file: ./.secrets/db_password这里让 shell 从只读 secret 文件读取合成密码,再以 Liquibase 环境变量交给同一进程;$$ 防止 Compose 在宿主机提前展开。生产环境应由 secret manager 注入短期凭据,并确保命令和日志不回显。挂载 changelog 为只读,镜像固定 digest,数据库容器只绑定内部网络。本地清理先停止迁移执行者,再删除合成库和 secret 文件。
CI 的职责不是直接把每个 PR 更新到生产,而是建立逐级证据:
jobs:
liquibase-check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@<APPROVED_COMMIT_SHA>
- uses: liquibase/setup-liquibase@<APPROVED_COMMIT_SHA>
with:
version: '<APPROVED_COMMUNITY_VERSION>'
edition: community
- run: liquibase --changelog-file=db/changelog/root.yaml validate
- run: liquibase --changelog-file=db/changelog/root.yaml --url="$JDBC_URL" --username="$DB_USER" update-sql --output-file=update.sql
env:
JDBC_URL: ${{ secrets.LAB_JDBC_URL }}
DB_USER: ${{ secrets.LAB_DB_USER }}
- uses: actions/upload-artifact@<APPROVED_COMMIT_SHA>
with:
name: liquibase-plan
path: update.sql固定 action 完整提交 SHA,给 job 最小仓库权限;数据库凭据只连接一次性影子库。合并门禁至少执行 validate、空库 update、重复 update、目标版本升级、rollback-sql、实际 rollback 与再次 update。生成 SQL 要做敏感信息扫描,并把 changelog commit、工具版本、驱动版本、数据库版本、context/label、目标 schema 摘要绑定到 artifact。
架构选型看变更模型,不看 YAML 还是 SQL
Liquibase 的优势是支持 XML/YAML/JSON 建模和 formatted SQL、多数据库 change type、precondition、context/label、SQL 预览及 Java/Spring 生态。团队需要同一套变更描述覆盖多种数据库,或需要较强条件化与结构化回滚时,它通常比纯版本 SQL 更合适。
代价也很明确:抽象 change type 生成的 SQL 仍受数据库实现影响;格式、插件和版本升级可能改变 checksum 或生成结果;复杂 context/label 会让实际执行集合难以推理;应用启动集成容易把迁移可用性绑到每个副本。单一数据库、SQL 审查能力成熟且偏好显式脚本的团队,可能更适合更简单的版本迁移工具;需要声明式 schema diff、lint 和计划审批时,应比较 Atlas 等方案。选型要用同一组真实 DDL、数据回填、失败注入、回滚和 CI 审批实验比较,而不是比较命令数量。
权限、容量与成本决定它能不能长期运行
迁移账号是高价值身份。按数据库和 schema 拆分账号,默认禁用跨业务读写;生产凭据由流水线短期获取,不下发开发机,不写入 artifact。DBA 或平台 owner 负责授权边界和数据库在线风险,服务 owner 负责兼容窗口与数据语义,变更作者负责正反脚本,评审者负责 SQL、回滚和选择器,发布 owner 负责目标、证据和停止条件。提交者与生产批准者不应是同一个不可审计身份。
容量评估不能只看 SQL 行数。新增索引会消耗排序空间、WAL/binlog、复制带宽和缓存;加列与默认值可能触发表重写;precondition snapshot 会读取大量元数据;长事务会扩大锁等待、undo 与复制延迟。影子库应接近生产 schema、数据库版本、扩展、数据分布和统计信息,并记录峰值磁盘、锁等待、复制积压与执行时间趋势。演示阈值不能直接成为生产阈值,停止条件由容量预算和 SLO 推导。
成本包括 Liquibase 发行许可、CI 分钟、临时数据库、影子数据脱敏、制品存储、审查人力、故障演练和升级验证。Secure 能力能减少部分自建策略成本,但不能替代数据库恢复、职责分离和数据治理。引入扩展、JDBC 驱动和自定义 change class 时,还要固定来源、版本、摘要与 SBOM,并在升级前验证 changelog 解析、checksum 和生成 SQL 是否变化。
清理实验,也要证明锁、记录和数据都被收回
实验结束先确认没有 Liquibase 进程持锁,再删除一次性容器和网络:
docker exec "$DB" psql -U lb_runner -d catalog -c \
'select id,locked,lockgranted,lockedby from databasechangeloglock;'
docker stop "$DB"
docker network rm "$NET"
rm -f checksum.out artifacts/update.sql artifacts/rollback.sql
unset LIQUIBASE_IMAGE LIQUIBASE_COMMAND_URL LIQUIBASE_COMMAND_USERNAME若实验使用命名卷,还要在确认没有需要保留的故障证据后显式删除卷。共享环境不能照搬 docker stop 或删除 schema;退出 Liquibase 时要冻结新变更、导出 changelog 与跟踪表、验证数据库真实结构、撤销迁移身份、清理 CI secret 和缓存,并明确接替工具如何继承历史。删除跟踪表只会删除治理证据,不会回退数据库。
长期稳定的数据库变更链应满足一组可查询的不变量:已执行 changeSet 不被原地修改;每次发布只有一个授权执行者;context 与 label 参数不能为空且可追溯;生成 SQL、回滚 SQL、影子库结果和批准记录绑定同一提交;失败后能从锁、数据库会话、真实对象和执行记录定位中间状态;结构回滚与数据恢复分别演练;迁移账号不能成为应用常驻账号;工具和驱动升级会重新生成计划并做差异审查。
做到这些以后,Liquibase 才不只是应用启动时的一段自动 SQL,而是一条能够回答“谁准备了什么、数据库实际执行了什么、失败停在哪里、怎样安全继续或退出”的工程证据链。
