Liquibase SQL格式化与数据库迁移版本控制策略 - 第2节
2.1 ChangeLog:迁移脚本的入口清单
ChangeLog 是 Liquibase 迁移的顶层入口文件,它定义了一个有序的数据库变更清单。Liquibase 通过解析 ChangeLog 来获取所有需要执行的数据库变更,并按照定义的顺序逐一应用。
ChangeLog 支持多种格式,包括 XML、YAML、JSON 以及 SQL 格式化文件。在 SQL 格式化策略中,ChangeLog 本身是一个遵循特定注释约定的 .sql 文件,通过 --liquibase formatted sql 声明开头,后续通过 --changeset 注释划分变更集。
ChangeLog 的典型结构(SQL 格式化)
--liquibase formatted sql
--changeset author:runOnChange:true
--comment: 创建用户表
CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100)
);
--changeset author:context:test
--comment: 插入测试数据
INSERT INTO users (id, username, email) VALUES (1, 'test_user', 'test@example.com');
每个 ChangeLog 文件可以包含多个 --changeset 注释块,每个块代表一个原子变更单元。Liquibase 会按文件中的物理顺序从上到下依次处理这些变更集。
2.2 ChangeSet:原子化的数据库变更单元
ChangeSet 是 Liquibase 中最小的执行单元,代表一个不可分割的数据库变更操作。每个 ChangeSet 必须包含一个唯一的标识符,由 作者(author) 和 变更集 ID(id) 共同组成,Liquibase 使用 author:id 作为全局唯一键来追踪执行状态。
| 属性 | 说明 | 示例值 |
|---|---|---|
| author | 变更集的作者标识,通常使用用户名或邮箱 | zhangsan、devops_team |
| id | 变更集的序号或描述性标识,在同一作者下唯一 | 001、create-user-table |
| runOnChange | 当 ChangeSet 内容发生变化时是否重新执行 | true / false |
| context | 指定该变更集仅在特定上下文中执行 | test、production |
| runAlways | 是否每次运行都强制执行该变更集 | true / false |
在 SQL 格式化文件中,ChangeSet 的声明方式如下:
--changeset zhangsan:create-user-table
--comment: 创建用户主表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
--changeset zhangsan:add-email-index
--comment: 为用户邮箱添加索引
CREATE INDEX idx_users_email ON users(email);
Liquibase 在执行时,会检查 DATABASECHANGELOG 表中是否已存在 zhangsan:create-user-table 和 zhangsan:add-email-index 的记录。如果已存在且内容未变化,则跳过;如果不存在或内容有变化(且 runOnChange=true),则执行。
2.3 DATABASECHANGELOG 表:变更执行的历史账本
DATABASECHANGELOG 表是 Liquibase 自动在目标数据库中创建的一张元数据表,用于记录所有已执行的 ChangeSet 的详细信息。它是 Liquibase 实现增量迁移和幂等性的核心保障。
每次成功执行一个 ChangeSet 后,Liquibase 都会向该表插入一条记录。下次运行时,Liquibase 通过查询该表来判断哪些 ChangeSet 已经执行过,从而避免重复执行。
DATABASECHANGELOG 表结构
| 字段名 | 类型 | 说明 |
|---|---|---|
| ID | VARCHAR | ChangeSet 的 id 属性值 |
| AUTHOR | VARCHAR | ChangeSet 的 author 属性值 |
| FILENAME | VARCHAR | ChangeSet 所在的 ChangeLog 文件路径 |
| DATEEXECUTED | TIMESTAMP | 执行完成的时间戳 |
| ORDEREXECUTED | INT | 全局执行顺序号,单调递增 |
| MD5SUM | VARCHAR | ChangeSet 内容的 MD5 校验和,用于检测变更 |
| DESCRIPTION | VARCHAR | 变更类型的描述(如 "Create Table") |
| COMMENTS | VARCHAR | ChangeSet 中的 comment 内容 |
| TAG | VARCHAR | 标记名称,用于回滚定位 |
| LIQUIBASE | VARCHAR | 执行该变更的 Liquibase 版本 |
| CONTEXTS | VARCHAR | 执行时生效的上下文 |
| LABELS | VARCHAR | 执行时生效的标签 |
| DEPLOYMENT_ID | VARCHAR | 一次部署会话的唯一标识 |
DATABASECHANGELOG 表中的记录。如果手动删除或修改记录,会导致 Liquibase 认为某些 ChangeSet 未执行或校验和不匹配,从而引发意外的重复执行或执行失败。
2.4 DATABASECHANGELOGLOCK 表:并发执行的互斥锁
DATABASECHANGELOGLOCK 表是 Liquibase 用于实现分布式环境下的互斥锁机制的表。当多个 Liquibase 实例(如 CI/CD 流水线、多个开发人员本地执行)同时尝试对同一个数据库执行迁移时,该表确保同一时间只有一个实例能够执行变更,防止并发冲突导致的数据不一致。
锁机制的工作流程
- 获取锁: Liquibase 在开始执行任何 ChangeSet 之前,首先尝试向
DATABASECHANGELOGLOCK表插入一条锁记录。如果插入成功,表示获取锁成功,可以继续执行。 - 执行变更: 持有锁的实例正常执行所有待处理的 ChangeSet,并更新
DATABASECHANGELOG表。 - 释放锁: 所有变更执行完毕后,Liquibase 删除锁记录,释放锁资源。
- 等待重试: 如果另一个实例尝试获取锁时发现锁已被占用,它会等待一段时间(默认 60 秒)后重试,直到超时或锁被释放。
DATABASECHANGELOGLOCK 表结构
| 字段名 | 类型 | 说明 |
|---|---|---|
| ID | INT | 锁记录的唯一标识,通常固定为 1 |
| LOCKED | BOOLEAN | 是否被锁定(1 表示锁定,0 表示未锁定) |
| LOCKGRANTED | TIMESTAMP | 锁被授予的时间 |
| LOCKEDBY | VARCHAR | 持有锁的主机名或实例标识 |
liquibase releaseLocks 命令手动释放锁,或直接删除 DATABASECHANGELOGLOCK 表中的记录。
2.5 核心概念关系图
下图展示了 ChangeLog、ChangeSet、DATABASECHANGELOG 表和 DATABASECHANGELOGLOCK 表之间的协作关系:
2.6 四者协作流程
下图以时序方式展示了 Liquibase 在一次迁移执行过程中,四个核心概念之间的交互顺序:
2.7 核心概念对比总结
| 概念 | 角色 | 存储位置 | 是否可手动修改 | 关键作用 |
|---|---|---|---|---|
| ChangeLog | 变更清单入口 | 文件系统(.sql / .xml / .yaml / .json) | 是(版本控制管理) | 定义变更集的顺序和分组 |
| ChangeSet | 原子变更单元 | ChangeLog 文件内部 | 是(版本控制管理) | 封装具体的 DDL/DML 操作 |
| DATABASECHANGELOG | 执行历史账本 | 目标数据库 | 否(Liquibase 自动维护) | 记录已执行变更,防止重复执行 |
| DATABASECHANGELOGLOCK | 并发互斥锁 | 目标数据库 | 否(可通过 releaseLocks 释放) | 确保同一时间只有一个实例执行迁移 |