4. Liquibase核心概念与入门:changelog、changeset、databaseChangeLog锁、回滚机制
好,咱们进入正题。Liquibase 这东西,说白了就是一个数据库版本控制器。跟 Git 管代码一样,它管你的数据库结构变化。我最早接触它是在一个金融项目里,那时候每次上线都要手动执行 SQL 脚本,出过好几次事故。后来用了 Liquibase,虽然学习曲线有点陡,但用顺了之后,真香。
这一章,咱们把 Liquibase 的四个核心概念掰开揉碎讲清楚。你只要搞懂 changelog、changeset、锁机制和回滚,基本就能上手干活了。
4.1 changelog:你的数据库变更清单
changelog 是什么?它就是一份清单,记录了所有要对数据库做的变更。Liquibase 支持 XML、YAML、JSON 和 SQL 四种格式。我个人习惯用 XML,结构清晰,IDE 还有自动补全。
一个典型的 changelog 长这样:
<?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.9.xsd">
<include file="changesets/001-create-user-table.xml"/>
<include file="changesets/002-add-email-column.xml"/>
</databaseChangeLog>
看到没?changelog 本身不直接写 SQL,它通过 <include> 把一个个 changeset 文件组合起来。这样做的好处是,每个变更文件职责单一,好维护。
4.2 changeset:最小变更单元
changeset 是 Liquibase 里最核心的概念。一个 changeset 代表一次原子性的数据库变更。它必须有两个属性:id 和 author。这两个值加上文件名,共同构成 changeset 的唯一标识。
来看一个实际的 changeset:
<changeSet id="1" author="zhangsan">
<createTable tableName="user">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="username" type="varchar(50)">
<constraints unique="true" nullable="false"/>
</column>
<column name="created_at" type="timestamp">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
这里用的是 Liquibase 的 XML 标签,不是原生 SQL。好处是跨数据库兼容——同一份 changelog 可以在 MySQL、PostgreSQL、Oracle 上跑,Liquibase 会自动翻译成对应的方言。
当然,你也可以直接用 SQL:
<changeSet id="2" author="zhangsan">
<sql>
CREATE INDEX idx_user_email ON user(email);
</sql>
</changeSet>
4.3 databaseChangeLog 锁:并发安全的保障
多个人同时执行 Liquibase 怎么办?会不会冲突?
Liquibase 用了一张锁表来解决这个问题。默认情况下,它会在你的数据库里创建两张表:DATABASECHANGELOG 和 DATABASECHANGELOGLOCK。
DATABASECHANGELOG 记录所有已执行的 changeset,包括 id、author、文件名、MD5 校验和、执行时间、执行耗时等。
DATABASECHANGELOGLOCK 就是锁表,只有一行记录。当 Liquibase 开始执行变更时,它会先尝试获取锁。获取成功才能继续,否则就等待或报错。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| DATABASECHANGELOG | 记录已执行的变更历史 | ID, AUTHOR, FILENAME, MD5SUM, DATEEXECUTED |
| DATABASECHANGELOGLOCK | 保证并发执行安全 | ID, LOCKED, LOCKGRANTED, LOCKEDBY |
锁的机制是这样的:
- Liquibase 启动,检查锁表。
- 如果锁未被占用,写入锁记录(LOCKED=1),开始执行变更。
- 变更执行完毕,释放锁(LOCKED=0)。
- 如果锁已被占用,等待超时或直接失败。
避坑指南:我曾经遇到过一个问题——某个 changeset 执行到一半,进程被 kill 了。结果锁没释放,后续所有部署都卡住了。解决方案是手动去数据库里把 LOCKED 字段改成 0。所以,如果你的 CI/CD 流水线经常被中断,建议设置合理的超时时间,或者加个重试机制。
下面这张图展示了 Liquibase 执行变更的完整流程:
4.4 回滚机制:做错事了怎么办?
变更执行错了,或者上线后发现有问题,需要回退。Liquibase 提供了三种回滚方式。
4.4.1 自动回滚
有些 changeset 类型,Liquibase 能自动生成回滚语句。比如 <createTable> 对应的回滚就是 DROP TABLE。你只需要在 changeset 里声明:
<changeSet id="3" author="zhangsan">
<createTable tableName="product">
<column name="id" type="bigint"/>
<column name="name" type="varchar(100)"/>
</createTable>
<rollback>
DROP TABLE product;
</rollback>
</changeSet>
嗯,这里要注意——虽然 Liquibase 能自动推断回滚,但我建议你总是显式写 rollback 标签。为什么?因为自动推断不一定准确,尤其是复杂场景下。
4.4.2 手动回滚
对于 <sql> 标签或者自定义变更,Liquibase 没法自动生成回滚。这时候你必须手动写:
<changeSet id="4" author="zhangsan">
<sql>
ALTER TABLE user ADD COLUMN age INT;
</sql>
<rollback>
ALTER TABLE user DROP COLUMN age;
</rollback>
</changeSet>
4.4.3 回滚命令
执行回滚时,Liquibase 提供了几个命令:
| 命令 | 说明 | 示例 |
|---|---|---|
| rollbackCount | 回滚最近 N 个 changeset | liquibase rollbackCount 3 |
| rollbackToDate | 回滚到指定日期之前 | liquibase rollbackToDate 2024-01-15 |
| rollback | 回滚到指定 tag | liquibase rollback v1.0 |
<tagDatabase tag="v1.0"/>。这样万一出问题,直接 liquibase rollback v1.0 就能回到上线前的状态。我在生产环境就是这么干的,回滚速度从半小时缩短到几秒钟。
4.5 实战:第一个 Liquibase 项目
光说不练假把式。咱们快速搭一个项目试试。
首先,创建一个 db/changelog/db.changelog-master.xml:
<?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.9.xsd">
<changeSet id="1" author="zhangsan">
<createTable tableName="department">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(100)">
<constraints nullable="false"/>
</column>
</createTable>
<rollback>
DROP TABLE department;
</rollback>
</changeSet>
<changeSet id="2" author="zhangsan">
<addColumn tableName="department">
<column name="manager_id" type="bigint"/>
</addColumn>
<rollback>
ALTER TABLE department DROP COLUMN manager_id;
</rollback>
</changeSet>
<changeSet id="3" author="zhangsan">
<tagDatabase tag="v1.0"/>
</changeSet>
</databaseChangeLog>
然后执行:
liquibase --changeLogFile=db/changelog/db.changelog-master.xml \
--url=jdbc:mysql://localhost:3306/mydb \
--username=root \
--password=root \
update
执行成功后,去数据库看看,DATABASECHANGELOG 表里多了三条记录。想回滚?简单:
liquibase --changeLogFile=db/changelog/db.changelog-master.xml \
--url=jdbc:mysql://localhost:3306/mydb \
--username=root \
--password=root \
rollback v1.0
搞定。department 表被删了,manager_id 列也没了,数据库回到了 v1.0 标签时的状态。
核心要点回顾:
- changelog 是变更清单,通过 include 组合多个 changeset 文件。
- changeset 是原子变更单元,id + author + 文件名 唯一标识。
- DATABASECHANGELOGLOCK 保证并发安全,但要注意死锁场景。
- 回滚有三种方式:自动、手动、按 tag/日期/数量。
- 已执行的 changeset 不能修改,只能追加新的。