在数据库变更管理中,Liquibase 通过 changeset 记录每一次结构或数据调整,而 tag 则用来标记某个时间点的整体变更状态。当新版本上线后出现故障,我们可以利用之前打好的 tag 将数据库回滚到稳定版本。下面直接看一个典型的实践流程。

一、changeset 与 tag 的基础概念
changeset 是 Liquibase 最小的可执行变更单元,每个 changeset 拥有唯一的 id 与 author 组合,并附带文件路径作为唯一标识。Liquibase 会在数据库的 DATABASECHANGELOG 表中记录已执行的 changeset,保证重复执行时不会出错。一个 changeset 可以包含建表、加列、插数据等多种操作,但建议保持原子性,以便于回滚。
tag 则是附加在 DATABASECHANGELOG 表某行记录上的标签名,它不代表具体的变更内容,而是指向某一个 changeset 执行后的数据库状态。你可以把 tag 理解为“已验证版本”的书签。只有当 tag 存在时,rollback 命令才能以 tag 为终点,逆序补偿后续 changeset。没有 tag,就只能按数量或日期回滚,风险更高。
1.1 如何定义一个简单的 changeset
在 XML 格式的 changelog 中,changeset 写法如下。注意每个 changeset 最好写明 rollback 指令,否则 Liquibase 只能做有限自动回滚。
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
<changeSet id="1" author="zhang">
<createTable tableName="user">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true"/>
</column>
<column name="name" type="varchar(50)"/>
</createTable>
<rollback>
<dropTable tableName="user"/>
</rollback>
</changeSet>
</databaseChangeLog>
上面的 changeset 创建了 user 表,并在 rollback 标签中指明回滚时直接删表。若省略 rollback,Liquibase 对 createTable 可自动生成 dropTable,但复杂变更往往无法自动逆操作,因此显式声明更稳妥。
二、为变更打 tag 的常用方式
打 tag 的本质是在当前已应用的最新 changeset 上写入标签。常用方式有三种:命令行、Maven 插件、以及在 changelog 中通过 tagDatabase 变更类型声明。生产环境推荐在每次发布前独立打 tag,而不是混在业务 changeset 里。
2.1 使用命令行打 tag
假设已配置好 liquibase.properties,执行下面命令即可将当前状态标记为 v1.0:
liquibase --username=root --password=root --url=jdbc:mysql://127.0.0.1:3306/demo --driver=com.mysql.cj.jdbc.Driver tag v1.0
命令执行后,DATABASECHANGELOG 表中最新一行记录的 TAG 字段会变为 v1.0。此后无论再跑多少个 changeset,只要不覆盖该 tag,就能随时回滚到 v1.0 对应的库结构。
2.2 在 changelog 中嵌入 tag
也可以在 changelog 文件里用 tagDatabase 显式留痕,适合希望 tag 随代码版本管理的团队:
<changeSet id="tag-v1.0" author="release">
<tagDatabase tag="v1.0"/>
</changeSet>
这种写法的好处是 tag 本身也是 changeset,拥有执行记录;缺点是若后续有人手动删 changeset,tag 会丢失。因此发布流水线中通常用外部命令打 tag 更可控。
三、基于 tag 的回滚操作实践
当 v1.1 版本上线后发现问题,而 v1.0 是稳定 tag,就可以执行回滚。Liquibase 会找到 tag 之后的所有 changeset,按相反顺序执行它们的 rollback 逻辑。
3.1 回滚到指定 tag
命令行回滚语法非常直观:
liquibase --username=root --password=root --url=jdbc:mysql://127.0.0.1:3306/demo rollback v1.0
该命令不会删除 v1.0 之前的变更,只会撤销 v1.0 之后 applied 的 changeset。例如 v1.0 之后新增了 order 表和两条数据变更,它们都会被对应 rollback 语句抵消。执行完毕后,DATABASECHANGELOG 表中那些 changeset 的 EXECTYPE 会标记为 ROLLEDBACK。
3.2 回滚的注意事项与坑点
第一,如果某个 changeset 没有写 rollback,且 Liquibase 无法自动推断(如自定义 SQL 更新),回滚会直接报错并中止。第二,tag 只能回退结构变更与有明确补偿的数据变更,若 changeset 中插入的数据已被业务修改,回滚可能丢失新数据。因此关键表变更前,务必先备份或采用灰度。
第三,不要对同一个 tag 名重复打标。Liquibase 允许覆盖,但会造成版本锚点混乱。建议 tag 命名遵循发布号规则,如 v1.2.3,并在 CI 脚本中校验 tag 不存在后再打。
四、结合 changeset 属性提升回滚安全性
除了 tag,changeset 上的 runOnChange、preConditions 等属性也能辅助回滚治理。比如用 preConditions 检查表是否存在,避免回滚时因环境差异失败。
4.1 使用 preConditions 保护回滚
下面示例要求回滚前 user 表必须存在,否则跳过而非报错:
<changeSet id="2" author="zhang">
<preConditions onFail="MARK_RAN">
<tableExists tableName="user"/>
</preConditions>
<addColumn tableName="user">
<column name="age" type="int"/>
</addColumn>
<rollback>
<dropColumn tableName="user" columnName="age"/>
</rollback>
</changeSet>
这样即使在部分环境未执行原 changeset,回滚也不会因为缺表而中断整个流程。配合 tag 使用,能让跨环境回滚更健壮。
4.2 回滚演练建议
在测试库定期重演“打 tag 到新版本再回滚”的链路,验证每个 changeset 的 rollback 正确。只有演练过的 tag 才允许在生产发布中使用。通过这种机制,Liquibase 的 tag 与 changeset 就能成为数据库版本控制的可靠基石。
总结来看,tag 提供时间点锚点,changeset 提供可逆操作单元,二者配合让数据库回滚从手工脚本进化为可追溯、可重复的工程化流程。团队应将其纳入标准发布规范,从而降低线上事故恢复时间。