2. Liquibase 核心概念:Changelog、Changeset、锁表与元数据表
在深入排错之前,必须彻底理解 Liquibase 的四个核心构件。它们是增量迁移的骨架,也是元数据锁冲突和 GC 停顿的根源。下面逐一拆解其结构、职责与潜在风险。
2.1 Changelog:变更清单的主控文件
Changelog 是 Liquibase 的入口文件,定义了数据库变更的顺序和逻辑。它通常是一个 YAML、XML、JSON 或 SQL 文件,通过 include 或 includeAll 指令组织多个 Changeset。
| 属性 | 说明 | 典型值 |
|---|---|---|
| databaseChangeLog | 根元素,声明命名空间和 schema 版本 | xmlns="http://www.liquibase.org/xml/ns/dbchangelog" |
| include | 引入单个 changelog 文件 | file="changelogs/001-schema.xml" |
| includeAll | 按文件名排序批量引入目录下所有文件 | path="changelogs/" |
| logicalFilePath | 覆盖文件路径,用于避免路径变更导致重复执行 | logicalFilePath="v1.0/changelog.xml" |
排错要点: 当使用
includeAll 时,Liquibase 会扫描整个目录并排序。如果目录下有大量文件(如 500+),首次解析 changelog 的 CPU 开销会显著增加,间接拉长事务持有锁的时间。
2.2 Changeset:原子变更单元
每个 Changeset 代表一个不可分割的数据库变更,由 id、author 和 filePath 三元组唯一标识。Liquibase 通过计算 Changeset 的 MD5 校验和来检测是否被篡改。
<changeSet id="create-user-table" author="dba">
<createTable tableName="user">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(100)">
<constraints nullable="false"/>
</column>
<column name="email" type="varchar(255)"/>
</createTable>
</changeSet>
| 属性 | 作用 | 排错关注点 |
|---|---|---|
| id | 变更集唯一标识 | 避免与历史 id 重复,否则会触发校验和错误 |
| author | 变更作者 | 多人协作时需统一命名规范 |
| runAlways | 每次迁移都执行,即使已记录 | 高并发下可能重复获取锁,增加锁竞争 |
| runOnChange | 仅当 checksum 变化时重新执行 | 频繁修改会导致大量重新执行,延长锁持有时间 |
| context | 按环境过滤执行 | 生产环境误配 context 可能导致变更跳过或重复 |
2.3 DATABASECHANGELOG 锁表:并发控制的命门
Liquibase 使用 DATABASECHANGELOGLOCK 表实现分布式锁,确保同一时间只有一个 Liquibase 实例执行迁移。这是整个排错课程的核心焦点。
关键风险: 锁表机制依赖数据库行级锁。当迁移事务长时间不释放(如因 GC 停顿或慢 SQL),其他实例会持续轮询等待,导致应用启动阻塞甚至雪崩。
-- DATABASECHANGELOGLOCK 表结构(MySQL 示例)
CREATE TABLE DATABASECHANGELOGLOCK (
ID INT NOT NULL,
LOCKED BIT(1) NOT NULL,
LOCKGRANTED DATETIME NULL,
LOCKEDBY VARCHAR(255) NULL,
PRIMARY KEY (ID)
);
-- 锁获取流程(伪代码)
INSERT INTO DATABASECHANGELOGLOCK (ID, LOCKED) VALUES (1, 1)
ON DUPLICATE KEY UPDATE LOCKED = 1;
-- 如果 LOCKED=1 且 LOCKGRANTED 超过超时阈值,则强制抢锁
| 字段 | 类型 | 说明 | 排错价值 |
|---|---|---|---|
| ID | INT | 锁记录主键,固定为 1 | 始终只有一行 |
| LOCKED | BIT/BOOLEAN | 1 表示已锁定,0 表示空闲 | 检查是否死锁 |
| LOCKGRANTED | DATETIME | 锁授予时间戳 | 计算锁持有时长,判断是否异常 |
| LOCKEDBY | VARCHAR | 持有锁的主机名和线程信息 | 定位哪个实例卡住了锁 |
实战排查命令:
如果
SELECT * FROM DATABASECHANGELOGLOCK WHERE ID = 1;如果
LOCKED=1 且 LOCKGRANTED 超过 5 分钟,大概率发生了锁未释放的异常。
2.4 DATABASECHANGELOG 表:变更历史审计
该表记录了所有已执行的 Changeset 及其校验和,是增量迁移的“飞行记录仪”。Liquibase 通过它判断哪些 Changeset 需要执行、哪些已跳过。
-- DATABASECHANGELOG 表核心字段
CREATE TABLE DATABASECHANGELOG (
ID VARCHAR(255) NOT NULL,
AUTHOR VARCHAR(255) NOT NULL,
FILENAME VARCHAR(255) NOT NULL,
DATEEXECUTED DATETIME NOT NULL,
ORDEREXECUTED INT NOT NULL,
EXECTYPE VARCHAR(10) NOT NULL, -- EXECUTED / RERAN / SKIPPED
MD5SUM VARCHAR(35) NULL,
DESCRIPTION VARCHAR(255) NULL,
COMMENTS VARCHAR(255) NULL,
TAG VARCHAR(255) NULL,
LIQUIBASE VARCHAR(20) NULL,
CONTEXTS VARCHAR(255) NULL,
LABELS VARCHAR(255) NULL,
DEPLOYMENT_ID VARCHAR(10) NULL
);
| 字段 | 排错用途 |
|---|---|
| MD5SUM | 校验 Changeset 内容是否被修改;不一致时抛出异常 |
| EXECTYPE | EXECUTED 正常执行;RERAN 重新执行;SKIPPED 条件跳过 |
| DATEEXECUTED | 精确到毫秒的执行时间,用于分析迁移耗时 |
| ORDEREXECUTED | 执行顺序号,可发现是否有乱序或回滚操作 |
| DEPLOYMENT_ID | 每次运行生成的唯一部署 ID,关联同一批次的所有变更 |
2.5 核心概念关系图
下图展示了 Changelog、Changeset、锁表和历史表之间的协作流程,以及锁竞争可能发生的环节。
2.6 增量迁移执行流程
理解完整执行流程,才能精准定位锁等待和 GC 停顿的插入点。
- 解析 Changelog: Liquibase 读取主文件,递归解析所有 include 文件,构建 Changeset 有序列表。
- 获取锁: 向
DATABASECHANGELOGLOCK表写入锁记录(ID=1, LOCKED=1)。如果锁已被占用,进入轮询等待(默认每秒重试一次,超时 60 秒)。 - 计算待执行 Changeset: 查询
DATABASECHANGELOG表,对比已执行记录,筛选出未执行或需要重新执行的 Changeset。 - 逐个执行 Changeset: 在每个 Changeset 执行前后,更新
DATABASECHANGELOG表(插入或更新 MD5SUM、EXECTYPE 等)。 - 释放锁: 所有 Changeset 执行完毕,将
DATABASECHANGELOGLOCK的 LOCKED 置为 0。
GC 停顿插入点: 步骤 4 中,如果单个 Changeset 包含大量 DDL 或 DML,且 JVM 堆内存不足,可能触发 Full GC。此时数据库事务未提交,锁表记录仍为 LOCKED=1,其他实例持续阻塞。
2.7 锁超时与强制解锁策略
生产环境中,锁未释放是常见故障。Liquibase 提供了两种应对机制:
| 策略 | 触发条件 | 风险 |
|---|---|---|
| 自动超时重试 | 等待锁超过 lockWaitTime(默认 60 秒) | 若持有锁的实例仍在运行,可能导致数据不一致 |
| 手动强制解锁 | DBA 执行 UPDATE DATABASECHANGELOGLOCK SET LOCKED=0 WHERE ID=1; | 如果原进程仍在执行变更,可能造成重复执行或数据损坏 |
强制解锁黄金法则: 必须先确认持有锁的进程已死(如通过
LOCKEDBY 字段找到主机,检查进程是否存在),再执行解锁。盲目解锁是生产事故的常见原因。