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 超过超时阈值,则强制抢锁
字段类型说明排错价值
IDINT锁记录主键,固定为 1始终只有一行
LOCKEDBIT/BOOLEAN1 表示已锁定,0 表示空闲检查是否死锁
LOCKGRANTEDDATETIME锁授予时间戳计算锁持有时长,判断是否异常
LOCKEDBYVARCHAR持有锁的主机名和线程信息定位哪个实例卡住了锁
实战排查命令:
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 内容是否被修改;不一致时抛出异常
EXECTYPEEXECUTED 正常执行;RERAN 重新执行;SKIPPED 条件跳过
DATEEXECUTED精确到毫秒的执行时间,用于分析迁移耗时
ORDEREXECUTED执行顺序号,可发现是否有乱序或回滚操作
DEPLOYMENT_ID每次运行生成的唯一部署 ID,关联同一批次的所有变更

2.5 核心概念关系图

下图展示了 Changelog、Changeset、锁表和历史表之间的协作流程,以及锁竞争可能发生的环节。

Liquibase 核心概念关系与锁竞争路径 Liquibase 核心概念关系与锁竞争路径 Changelog 主控文件 Changeset 集合 DATABASECHANGELOGLOCK 锁表(并发控制) DATABASECHANGELOG 历史审计表 获取锁 记录执行 数据库实例 A 数据库实例 B 数据库实例 C ⚠ 锁竞争热点: 多个实例同时尝试获取 DATABASECHANGELOGLOCK 行锁,只有一个成功。 若持有锁的实例因 GC 停顿或慢 SQL 未及时释放,其他实例将阻塞等待,导致应用启动超时。

2.6 增量迁移执行流程

理解完整执行流程,才能精准定位锁等待和 GC 停顿的插入点。

  1. 解析 Changelog: Liquibase 读取主文件,递归解析所有 include 文件,构建 Changeset 有序列表。
  2. 获取锁: 向 DATABASECHANGELOGLOCK 表写入锁记录(ID=1, LOCKED=1)。如果锁已被占用,进入轮询等待(默认每秒重试一次,超时 60 秒)。
  3. 计算待执行 Changeset: 查询 DATABASECHANGELOG 表,对比已执行记录,筛选出未执行或需要重新执行的 Changeset。
  4. 逐个执行 Changeset: 在每个 Changeset 执行前后,更新 DATABASECHANGELOG 表(插入或更新 MD5SUM、EXECTYPE 等)。
  5. 释放锁: 所有 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 字段找到主机,检查进程是否存在),再执行解锁。盲目解锁是生产事故的常见原因。