1 定义

Liquibase是一个用于 用于跟踪、管理和应用数据库变化的开源工具
通过日志文件(changelog)的形式记录数据库的变更(changeset),然后执行日志文件中的修改,将数据库更新或回滚(rollback)到一致的状态。
它的目标是提供一种数据库类型无关的解决方案,通过执行schema类型的文件来达到迁移。

2 背景

在实际上线的应用中,随着版本的迭代,经常会遇到需要变更数据库表和字段,必然会遇到需要对这些变更进行记录和管理,以及回滚等等;
同时只有脚本化且版本可管理,才能在让数据库实现真正的DevOps(自动化执行 + 回滚等)。在这样的场景下Liquibase等工具的出现也就成为了必然。

3 概念

  1. changelog文件:需要定义一堆xml文件,这些xml称为changelog文件。
    1. 项目启动时,会自动执行 changelog xml文件。
    2. 每个文件包含多个变化集合 changeSet,每个changeSet 记录了作者、改变的内容。
    3. 在执行 changeSet 时,由于改动的内容可以通过 Liquibase 提供的标签编写,所以无关具体的数据库产品(MySQL、Oracle 等),Liquibase 底层会根据实际使用的数据库类型转化为对应的 SQL。
      1. **标签:**例如 createTable、addColumn……等一些changeSet 中要修改的内容,通过标签进行操作。
  2. **执行锁:**Liquibase 具有执行锁,已经执行过的内容不会重复执行。

4 工作流程

SQL 变更记录到 changeset ,多个changeset变更组成了日志文件( changelog ),liquibase将changelog更新日志文件同步到指定的 RDBMS 中。

5 SpringBoot集成liquibase

5.1 maven依赖

<!--liquibase--><dependency><groupId>org.liquibase</groupId><artifactId>liquibase-core</artifactId><version>4.9.1</version></dependency>

5.2 添加changelog

在 src/main/resources 创建目录 db, db 目录用来存放 Liquibase 相关的 changelog 文件。
在 db/changelog 目录下创建 changelog-master.xml 文件,该文件为主配置文件,作用就是引入所有模块的 changelog 文件:
有更新时新增 include 标签和对应的文件即可。
指向changelog或文件夹的顺序是非常重要的。它需要告知Liquibase在运行SQL时的顺序。我们最好先运行changelog(其中包含了“create table(…)”),然后再运行使用该表的编译包。

<databaseChangeLogxmlns="http://www.liquibase.org/xml/ns/dbchangelog"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangeloghttp://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"><!-- 1.file:表示将此路径下 /sql/001_create_person_table.sql 目录中的 changelog 文件引入。假设 db 目录下还有其他模块(目录),继续通过  或  元素引入即可。 2.relativeToChangelogFile:为 true 时表示使用的是相对路径 而不是classpath--><include file="/sql/001_create_person_table.sql" relativeToChangelogFile="true"></include><changeSet id="T100-20221009-001" author="txg"><sqlFile path="/db/changelog/sql/002_create_person1_table.sql"></sqlFile></changeSet></databaseChangeLog>

5.2.1 标签详解

因为 liquibase 使用 DATABASECHANGELOG 这个表, 它会从 changelog 中按照顺序读取所有的 changesets, 对于每一个 changeset, 根据 id/author/filetpath 的组合查询这个 changeset 是否被执行过了。
如果这个 changeset 在数据库中被标记为已经执行了, 那么这个 changeset 就会被跳过, 除非有一个 runAlways 的属性设置为 true, 这个已经执行的 changeset 会被重新执行;changeset 中的所有 change 都执行完成之后,liquibase 将会在 DATABASECHANGELOG 中插入一条新的记录,id/author/filepath 以及这个 changeset 的 MD5Sum。
filepath 是一个路径, 用于定义 changelog-file 这个属性。 即使是同一个文件可以使用不同的路径引用,除非使用了 logical-file-path,否则还是被认为是不同的文件。
liquibase 试图在事务中执行每个 changeset,执行成功则提交, 失败则回滚。有些数据库会自动的提交事务,可能导致意想不到的问题。 因此,最佳实践是每个 changeset 中只有一个变更,除非你想在事务中执行一组非自动提交的变更,比如插入多个数据。

属性
属性解释
id指定一个字母-数字格式的标识符, 这个是必须的.
注意, 如果 id 中有 0 的话, 最好是用引号将 id 包裹起来, 比如 “1.10”. 这样的话可以保留 id 中所有的字符
author指定 changeset 的创建人, 这个是必须的
dbms指定要执行 changeset 的数据库的类型.When the migration step is running, it checks the database type against this attribute.
你可以作如下的事情:
  1. 列举多个数据库类型并用逗号分隔
  2. 在数据库类型前加感叹号可以不在指定的数据库中执行
  3. 可以使用关键字 all 和 none |
    | runAlways | 每次运行的时候都执行这个 changeset, 即使之前执行过 |
    | runOnChange | 在第一次的时候会被执行, 然后每次 changeset 有修改的时候会继续执行 |
    | Context | 根据运行时的设置控制 changeset 是否执行. 任何字符串都可以作为 context 的名字, 而且是大小写不敏感的 |
    | Labels | 根据运行时的设置控制 changeset 是否执行. 任何字符串都可以作为 context 的名字, 而且是大小写不敏感的 |
    | runInTransaction | 指定 changeset 是否作为一个单独的事务执行 (如果可能的话). 默认值为 true |
    | failOnError | 如果执行时报错的话这个 migration 是否失败, 默认值为 true |
    | runOrder | 控制 changeset 的执行顺序, 用的也少, 按顺序执行就完了 |
    | created | 存储日期, 版本或者任何字符串的值, 而不是 remarks |
    | ignore | 忽略这个 changeset 的执行 |
    | logical-file-path | 当创建 changesets 的唯一标识符时重写文件名和路径. 当需要移动或者重命名 changelog 时会用到这个值 |
子标签
子标签解释
comment指定一个字母-数字格式的标识符, 这个是必须的.
注意, 如果 id 中有 0 的话, 最好是用引号将 id 包裹起来, 比如 “1.10”. 这样的话可以保留 id 中所有的字符
preConditions指定 changeset 的创建人, 这个是必须的
validCheckSum给这个 changeset 指定一个有效的 changeset, 而不管数据库里面存储的是什么. 主要使用场景是你要修改已经执行的 changeset 又不想报错时使用.1:any 这个特殊的值将匹配任何的 checksum, 不且 changeset 修改时不会执行
rollback指定 sql 语句或者 Change Type 如何回滚这个 changeset
  1. file:表示要包含的changelog文件的路径,这个文件可以是LiquiBase支持的任意格式。表示将此路径下目录中的 changelog 文件引入;假设 db 目录下还有其他模块(目录),继续通过 或 元素引入即可。
  2. relativeToChangelogFile:如果为true,则表示file属性表示的文件路径是相对于根changelog而不是CLASSPATH的,默认为false。

注意: 目前没有解决重复引用和循环引用的问题,重复引用还好,LiquiBase在执行的时候可以判断重复,而循环引用会导致无限循环,需要注意!

指定的是changelog的目录,而不是为文件

5.3 添加sql文件

在 src/main/resources/db 创建目录 changelog/sql,用来存放数据库脚本文件;
例如,创建 001_create_person_table.sql 文件

  • 每个sql文件必须以– liquibase formatted sql注释开头
  • 每个changeset必须以–changeset author:id注释开头
-- liquibase formatted sql-- changeset txg:2023_03_05insert into departments values(1, '研发部');insert into departments values(2, '销售部');-- changeset ttt:2023_03_06insert into departments values(3, '后勤部');-- changeset ggg:2023_03_06DELETE FROM departments WHERE DEPARTMENT_NAME = '研发部'

5.4 添加liquibase配置

配置开启 liquibase,并指定主文件的路径

spring:datasource:url: jdbc:oracle:thin:@172.20.0.7:1521:ctsdbdriver-class-name: oracle.jdbc.driver.OracleDriverusername: CTS_DEVpassword: CTS_DEVliquibase:#是否开启liquibase(默认为true)enabled: true#配置文件的路径(默认为classpath:/db/changelog/db.changelog-master.yaml)change-log: classpath:/db/changelog/changelog-master.xml#是否先 drop schema(默认为false)drop-first: falseserver:port: 8088

5.4.1 配置项详解

5.5 启动服务

5.5.1 自动创建表

  • 服务启动成功后,会自动创建两张 liquibase 相关的表:DATABASECHANGELOG 和 DATABASECHANGELOGLOCK表。分别记录changelog的执行日志和锁日志。
  • LiquiBase在执行changelog中的changeSet时,会首先查看DATABASECHANGELOG表,若是已经执行过,则会跳过(除非changeSet的runAlways属性为true),若是没有执行过,则执行并记录changelog日志;
  • changelog中的一个changeSet对应一个事务,在changeSet执行完后commit,若是出现错误则rollback;
5.5.1.1 DATABASECHANGELOG
  • author和id唯一标识一个变化(ChangSet)
  • Liquibase 使用 databasechangelog 表来跟踪已运行的changeSet。
  • 该表将每个更改设置作为一行进行跟踪,由存储changelog文件的路径的id、author和filename列的组合标识。
  • Liquibase会对已经执行的changelog的每一个changeSet的内容进行md5计算,生成的值是databasechanglog表的MD5SUM字段。当重新启动Liquibase时,会对每个changeSet进行md5值计算,与databasechanglog表中的MD5SUM字段进行对比,如果不一致,说明changeSet值已经被修改,无法启动成功。

CREATE TABLE "CTS_DEV"."DATABASECHANGELOG" ("ID" VARCHAR2(255) NOT NULL ENABLE,-- changeSet中的id属性值"AUTHOR" VARCHAR2(255) NOT NULL ENABLE, -- changeSet中的author属性值"FILENAME" VARCHAR2(255) NOT NULL ENABLE, -- changelog的路径"DATEEXECUTED" TIMESTAMP (6) NOT NULL ENABLE, -- 执行changeSet的日期/时间"ORDEREXECUTED" NUMBER(*,0) NOT NULL ENABLE, -- 执行changeSet的顺序"EXECTYPE" VARCHAR2(10) NOT NULL ENABLE, -- changeSet是如何执行的描述"MD5SUM" VARCHAR2(35), -- 执行changeSet时的校验"DESCRIPTION" VARCHAR2(255), -- changeSet生成的可读的描述"COMMENTS" VARCHAR2(255), --changeSet的comment标签的值"TAG" VARCHAR2(255), --changeSet的跟踪对应于标签的操作"LIQUIBASE" VARCHAR2(20), -- 用于执行changeSet的Liquibase版本"CONTEXTS" VARCHAR2(255), "LABELS" VARCHAR2(255), "DEPLOYMENT_ID" VARCHAR2(10) ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGINGSTORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)TABLESPACE "USERS"
5.5.1.2 DATABASECHANGELOGLOCK
  • Liquibase 使用 databasechangeloglock 表确保一次只运行一个 Liquibase 实例。
  • 因为Liquibase 只是从 databasechangelog 表读取以确定需要运行的changeSet,因此,如果同时对同一数据库执行多个 Liquibase实例,则会发生冲突。如果多个开发人员使用相同的数据库实例,或者集群中有多个服务器在启动时自动运行 Liquibase,则可能会发生这种情况。
  • 如果 Liquibase 未干净地退出,则锁住的行可能会保留为锁定状态。可以通过运行UPDATE DATABASECHANGELOGLOCK SET LOCKED=0 清除当前锁

CREATE TABLE "CTS_DEV"."DATABASECHANGELOGLOCK" ("ID" NUMBER(*,0) NOT NULL ENABLE, -- 锁的id(primaryKey)"LOCKED" NUMBER(1,0) NOT NULL ENABLE, --如果Liquibase正在针对此数据库运行,则设置为"1""LOCKGRANTED" TIMESTAMP (6), -- 获取锁的日期和时间"LOCKEDBY" VARCHAR2(255), -- 被谁锁住的描述CONSTRAINT "PK_DATABASECHANGELOGLOCK" PRIMARY KEY ("ID")USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)TABLESPACE "USERS"ENABLE ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGINGSTORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)TABLESPACE "USERS"


5.6 验证

成功执行sql文件里的脚本:

6 Java代码实现

方法

public static void main(String[] args) {String dbUrl = "jdbc:oracle:thin:@172.20.0.7:1521:ctsdb";String changeLogFile = "classpath:/db/changelog/changelog-master1.xml";String dbUsername = "CTS_DEV";String dbPassWord = "CTS_DEV";liquiBaseStart(dbUrl,dbUsername,dbPassWord,changeLogFile);}public void liquiBaseStart(String dbUrl,String dbUsername,String dbPassWord,String changeLogFile){// 加载驱动try {Class.forName("oracle.jdbc.driver.OracleDriver");// 连接数据库Connection connection = DriverManager.getConnection(dbUrl, dbUsername, dbPassWord);JdbcConnection jdbcConnection = new JdbcConnection(connection);// 初始化liquibaseLiquibase liquibase = new Liquibase(changeLogFile, new ClassLoaderResourceAccessor(), jdbcConnection);// update("服务运行环境"),会匹配changelog中changeset中的context标签liquibase.update("");connection.close();} catch (ClassNotFoundException e) {throw new RuntimeException(e);} catch (SQLException e) {throw new RuntimeException(e);} catch (LiquibaseException e) {throw new RuntimeException(e);}}

7 plugin

该插件可以根据数据库逆向生成 changlog 文件,从已有的数据库生成xml配置信息

  1. 生成xml文件
    1. 通过idea的maven功能,找到 liquibase plugin,双击 liquibase:generateChangeLog 选项,执行完成之后就会在 properties 文件中配置的 outputChangeLogFile 路径生成对应的xml文件
  2. 生成数据库修改文档
    1. 双击liquibase plugin面板中的liquibase:dbDoc选项,会生成数据库修改文档,默认会生成到target目录中
  3. 发布changelog
    1. 之前我们对changelog的编辑都需要通过启动项目来运行changelog,有时候我们可能想不重启项目便能将修改发布运行到数据库中
    2. 双击liquibase plugin面板中的liquibase:update选项,便可以将修改同步到数据库中

7.1 配置项

配置数据库连接信息和Liquibase生成规则配置。

<!-- 接着引入liquibase插件, 如果你的数据库已经有表和数据了可以使用该插件反向生成xml文件 --><plugin><groupId>org.liquibase</groupId><artifactId>liquibase-maven-plugin</artifactId><version>3.5.1</version><configuration><propertyFile>src/main/resources/db/liquibase.properties</propertyFile><propertyFileWillOverride>true</propertyFileWillOverride><outputChangeLogFile>src/main/resources/liquibase/changelog/changelog_init.xml</outputChangeLogFile></configuration></plugin>

8 管理

根据发布进行管理
  1. 每个发布新建一个文件夹,所有发布相关的 ChangeSet 文件以及数据初始化文件,均放在些文件夹中。
  2. 每个发布新建一个 master.xml。此 master.xml 中,include 本次发布需要执行的 ChangeSet 文件
  3. 根据开发小组独立 ChangeSet文件(可选)
  4. 根据功能独立 ChangeSet 文件。例如 user.xml, company.xml
esources|-liquibase|-user| |- master.xml| |- release.1.0.0| | |- release.xml| | |- user.xml -- 用户相关表ChangeSet| | |- user.csv -- 用户初始化数据| | |- company.xml -- 公司相关表ChangeSet| |- release.1.1.0| | |- release.xml| | |- ...
模块化管理

首先说明一下 Spring Boot 中 Liquibase 默认是如何执行以及执行结果。

  1. 在启动时,LiquibaseAutoConfiguration 会根据默认配置初始化 SpringLiquibase
  2. SpringLiquibase.afterPropertiesSet()中执行 ChangeSet 文件
  3. 第一次跑 ChangeSets 的时候,会在数据库中自动创建两个表databasechangelog和databasechangeloglock

因此我们可以认为一个 SpringLiquibase 执行为一个模块。
引入多模块管理时,基于上节文件管理规范,我们基于模块管理再做下调整。

resources|-liquibase|-user| |- master.xml| |- release.1.0.0| | |- release.xml| | |- user.xml -- 用户相关表ChangeSet| | |- user.csv -- 用户初始化数据| | |- company.xml -- 公司相关表ChangeSet| |- release.1.1.0| | |- release.xml| | |- ...|- order| |- master.xml| |- release.1.0.0| | |- ...

9 基本规范

  • ChangeSet id 使用[任务ID]-[日期]-[序号],如 T100-20181009-001
  • ChangeSet 必须填写 author
  • Liquibase 禁止对业务数据进行 sql 操作
  • 使用时,禁止包含 schema 名称
  • Liquibase 禁止使用存储过程
  • 所有表,列要加 remarks 进行注释
  • 已经执行过的 ChangeSet 严禁修改。
  • 不要随便升级项目 liquibase 版本,特别是大版本升级。不同版本 ChangeSet MD5SUM 的算法不一样。

10 常见问题

10.1 Waiting for changelog lock

如果启动服务时,控制台提示如下信息:
Liquibase - Waiting for changelog lockWaiting for changelog lock....
通常是由于 Liquibase 在重构数据库时使数据库死锁。解决方法如下:
1 查看锁住数据库的id:
SELECT * FROM DATABASECHANGELOGLOCK where LOCKED = true;
2 解锁:
UPDATE DATABASECHANGELOGLOCKSET locked=0, lockgranted=null, lockedby=nullWHERE id={id}
{id} 为第一步中查询出来对应记录的id。

10.2 change set check sums…

liquibase 在处理 changeset 的时候, 会先计算校验和并将其存储到 DATABASECHANGELOG 这个表中. 在 changeset 运行之后, 如果 changeset 有任何修改 liquibase 都能通过对比校验和感知到.
如果已经运行的 changeset 发生了修改, 会导致校验和变化,liquibase 会退出执行并且报错:
Validation failed: change set check sums was: but is now: .
这是因为 liquibase 不知道你做了哪些变更. 如果确实是 changeset 需要修改, 而你又想忽略这个错误, 有两个选择:

手动修改 DATABASECHANGELOG 中的记录
第一个方法是手动修改 DATABASECHANGELOG 这个表中对应的记录, 可以将其设置为空值. 这个应该在所有的环境都被部署之后进行. 下次你再运行 liquibase 的时候, 将会更新 checksum 到正确的新值.

属性
第二个方法是给 changeset 增加一个 元素. 这个文本将原来报错的 md5 放到这里来.

runOnChange 属性
校验和通常会和 changeSet 的 runOnChange 属性结合使用. 有时候你并不想增加新的 changeset, 因为只需要知道当前版本, 但是你又想每次更新的时候使用.
runOnChange 默认是 false 的. 如果将其设置为 true 的话, 那么只要 changeset 有更新则会执行.


参考

Liquibase官网