项目需求
某项目由于历史原因,没有数据库版本控制功能,开发人员在添加或者修改表结构等操作时,需要手动归档sql,手动执行sql,效率低下且容易出错,最后,无数据库版本控制会导致产品上线后难以维护。基于以上原因,需要给该项目引入数据库版本控制功能。
技术选型
市面上有很多的数据库版本控制工具,通过筛选留下了三种开源的数据库版本管理工具作为候选:
- flyway
- liquibase
- dolt
flyway很久之前在其他项目有使用过,这次重新进行了调研对比,通过对比最终选择了liquibase。主要原因如下:
- dolt 功能不太符合我们的项目场景,它的思路是git的管理思路,最先pass掉
- flyway的开源功能不支持版本回退和基于基线的版本控制,必须商业版本才能支持,而liquibase是支持的,这点比较重要,能用免费的开源技术实现项目的商业价值就绝对不用付费的技术
- liquibase本身功能强大,支持主流数据库,支持sql/xml/json/yaml等多种配置方式,一次配置,多种数据库均可使用,官方文档也非常详细
liquibase的基本概念
Changelog Changeset
上述图片来自官方文档,展示了sql格式与xml格式的Changelog。一个Changelog可以包含多个Changeset,一个Changelog也可以同时包含其他的Changelog,一个Changeset表示一次数据变更,一个Changeset可以包含多个变更语句。由于本文主要讲解工程实践过程,详细的概念可以参考官方文档的基本概念。
实战操作
由于该项目之前一直使用sql维护schema,所以自然地采取了sql格式的Changelog进行数据库的版本控制,如果是新建项目,建议使用其他格式(xml/json/yaml)进行,因为那样可以方便的迁移到各种数据库中,使用sql就跟数据库产生了绑定关系。同时,使用sql还需要自己写回退部分的配置,而其他格式支持自动生成回退部分的代码,在大多数情况无需自己编写回退部分的配置。
版本控制的集成方案选择
liquibase本身是使用java编写的,我们需要改造的项目也是java项目,候选了两种方式作为liquibase的集成方案:一种是直接引用官方提供的jar包,使用java直接调用liquibase的api进行数据库的版本控制;第二种是使用官方编译好的版本,编写脚本进行数据库的版本控制。最终通过权衡选择了第二种方式,原因如下:
- 直接调用liquibase的api需要维护单独的工程代码,需要多创建一个代码仓库
- 直接调用liquibase的api需要自己编写代码,虽然自由度大,但是工作量相对会增加
- 官方编译好的版本,升级只需要升级编译好的软件制品即可,较为方便,另外,编写脚本工作量不大
- 功能够用即可,即使将来遇到更复杂的版本控制场景,转换成第一种方案的代价也并不高
工程化方案
- 确定版本控制的目录结构
- 确定功能,编写功能脚本
- 实现虚拟化部署
- 实现自动化部署(CI/CD)
项目中的migration目录结构
由于隐私问题,项目目录中隐去了具体代码部分,只剩余数据库版本控制部分的migration目录,所有跟数据库版本控制相关的文件均放在migration目录下。
- db:Changelog归档
- liquibase:官方release版本的软件制品,直接下载解压即可拥有,示例中为4.6.1版本
- Dockerfile:docker镜像的构建脚本
- sh脚本文件:支持升级与回退的脚本文件
下面详细介绍各个文件目录的功能。
db目录
db目录中混合了两种Changelog,sql格式与yaml格式,原因是因为官方支持的四种格式中只有sql格式不能包含其他Changelog,这块选择了yaml作为Changelog的入口。
001_create_tables_baseline.sql的Changelog示例,这块需要完全遵循官方规范,sql changelog开头必须是--liquibase formatted sql,每个changeset的写法为开头--changeset techgeeknext:create-tables,其中techgeeknext为作者名称,create-tables是changeset的id,changeset的id必须全局唯一,官方建议使用liquibase的项目组应该提前定好changeset的id命名规范,项目维护时每个人遵守规范即可。--rollback放在每个changeset的最后,用于编写回退规则。这块的sql使用的是官方示例。
--liquibase formatted sql
--changeset techgeeknext:create-tables
CREATE TABLE employee(
id INT PRIMARY KEY,
name VARCHAR(40)
);
CREATE TABLE branch(
id INT PRIMARY KEY,
name VARCHAR(40),
emp_id INT,
FOREIGN KEY(emp_id) REFERENCES employee(id) ON DELETE CASCADE
);
--rollback drop table if exists employee,branch;
baseline.yaml,这块建立的baseline.yaml是yaml格式的changelog,它包含了一个changeset和两个changelog,这块的changeset什么都没有实现,所以是一个空的changeset,不会对数据库造成任何影响,但是升级时会产生一条记录。这块的baseline.yaml的意义在于,该项目此前已经有相关在线环境,数据库实际上是有内容的,并不是全量更新,而是在已有的基线上增量更新,所以这块我创建了baseline.yaml,用来归档已经在数据库中的存在的sql内容。
databaseChangeLog:
- changeSet:
id: 1
author: xkadmin
- include:
file: DDL/001_create_tables_baseline.sql
relativeToChangelogFile: true
- include:
file: DML/001_insert_data_baseline.sql
relativeToChangelogFile: true
migration.yaml包含的是基线之外,后续增量更新的内容:
databaseChangeLog:
- include:
file: DML/002_insert_data.sql
relativeToChangelogFile: true
db.changelog-all.yaml包含的是baseline.yaml和migration.yaml,当全新环境安装需要全量sql更新的场景,那么直接调用db.changelog-all.yaml进行更新。
databaseChangeLog:
- include:
file: baseline/baseline.yaml
relativeToChangelogFile: true
- include:
file: migration/migration.yaml
relativeToChangelogFile: true
sh脚本
migration_on_baseline.sh
#!/usr/bin/env bash
echo "baseline migration start."
. /migration/var.sh
username=$1
password=$2
url=$3
baselineFile=db/changelogs/baseline/baseline.yaml
migrationFile=db/changelogs/migration/migration.yaml
echo "/migration/liquibase/liquibase --changeLogFile=$baselineFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password changelogSync"
/migration/liquibase/liquibase --changeLogFile=$baselineFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password changelogSync
echo "/migration/liquibase/liquibase --changeLogFile=$migrationFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password update"
/migration/liquibase/liquibase --changeLogFile=$migrationFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password update
echo "baseline migration finish."
该脚本用于已安装环境的增量更新,首先针对baseline.yaml使用changelogSync进行基线确定,实际上liquibase在执行这条命令时就是单纯在数据库中把baseline.yaml指定的changeset刷到记录表中,但是并不是执行sql,基线确认后,再使用update命令将migration.yaml中的changeset中的sql在数据库中进行执行,执行结束后,基于基线的更新就结束了,这种方式适用于已有环境的更新,本质就是基线sql不执行,只执行后续增量更新的sql。该功能在flyway中是收费功能,而liquibase是免费的,这也是选择liquibase的重要原因。
migration_all.sh
#!/usr/bin/env bash
. /migration/var.sh
username=$1
password=$2
url=$3
changeLogFile=db/changelogs/db.changelog-all.yaml
echo "/migration/liquibase/liquibase --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password update"
/migration/liquibase/liquibase --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password update
该脚本用于全量更新的场景,即全新安装环境,本质就是所有changelog的changeset中的sql都会在数据库中进行执行。db.changelog-all.yaml包含了系统从开始开发到至今的所有changelog。
rollback_count.sh
#!/usr/bin/env bash
. /migration/var.sh
username=$1
password=$2
url=$3
count=$4
changeLogFile=db/changelogs/db.changelog-all.yaml
echo "/migration/liquibase/liquibase --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password rollbackCount $count"
/migration/liquibase/liquibase --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password rollbackCount $count
回退脚本,这个脚本可以执行回退,通过传入count数量,可以实现基于changeset个数的回退,这个在后文可以观察实际执行结果去理解。
rollback_tag.sh
#!/usr/bin/env bash
. /migration/var.sh
username=$1
password=$2
url=$3
tag=$4
changeLogFile=db/changelogs/db.changelog-all.yaml
echo "/migration/liquibase/liquibase --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password rollback $tag"
/migration/liquibase/liquibase --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
--hub-mode=off --username=$username --url=$url --password=$password rollback $tag
基于tag标记的回退,这个很好理解,使用该命令传入tag,可以直接回退到标记位置的数据库版本。
tag.sh
#!/usr/bin/env bash
username=$1
password=$2
url=$3
md5=`cat md5`
echo "/migration/liquibase/liquibase --username=$username --password=$password --url=$url tag $md5"
/migration/liquibase/liquibase --username=$username --password=$password --url=$url tag $md5
用于打tag的脚本,在项目中的tag使用了db目录下面的所有文件和的md5值作为tag,这样保证了tag的唯一性。
var.sh
#!/usr/bin/env sh
logLevel=INFO
logFile=/var/logs/liquibase/liquibase.log
公共变量存放脚本。
migration_entrypoint.sh
#!/usr/bin/env bash
username=$1
password=$2
url=$3
type=$4
countOrTag=$5
echo url=$url
if [ $type == 0 ]
then
/migration/migration_on_baseline.sh $username $password $url && /migration/tag.sh $username $password $url
elif [ $type == 1 ]
then
/migration/migration_all.sh $username $password $url && /migration/tag.sh $username $password $url
elif [ $type == 2 ]
then
/migration/rollback_count.sh $username $password $url $countOrTag
elif [ $type == 3 ]
then
/migration/rollback_tag.sh $username $password $url $countOrTag
fi
全部程序入口,通过type判断到底是全量升级、增量升级、基于tag回退还是基于changeset数量的回退。migration脚本执行成功后会自动打上当前升级的tag。
Dockerfile
FROM www.xk.docker-registry.com:5501/openjdk:8u302-jre
MAINTAINER xk
WORKDIR /migration
ADD db db
ADD md5 md5
ADD liquibase liquibase
COPY *.sh /migration/
RUN chmod -R 777 /migration
VOLUME /var/logs/liquibase
RUN echo 'Asia/Shanghai' > /etc/timezone
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
ENTRYPOINT ["/migration/migration_entrypoint.sh"]
数据库版本控制功能docker化,打包所有相关内容到镜像中。
自动化版本控制
该项目存储在gitlab上,使用gitlab cicd来实现项目的自动化部署,那么版本控制也自然使用gitlab内置的cicd功能,具体方法是编写.gitlab-ci.yml。
stages:
- migration
docker build:
stage: migration
tags:
- compile
script:
- find migration/db/ -type f
- find migration/db/ -type f | xargs cat > migration/combine_file.txt && md5sum migration/combine_file.txt | awk '{print $1}' > migration/md5
- cat migration/md5
- docker build -f migration/Dockerfile -t www.xk.docker-registry.com:5501/migration:`cat migration/md5` migration/
- docker push www.xk.docker-registry.com:5501/migration:`cat migration/md5`
- docker rm -f migration || echo "No running migration task"
- echo "docker run --net=host -v /var/logs/liquibase:/var/logs/liquibase --name migration www.xk.docker-registry.com:5501/migration:`cat migration/md5` $MIGRATION_USER $MIGRATION_PASSWORD $MIGRATION_URL 1"
- docker run --net=host -v /var/logs/liquibase:/var/logs/liquibase --name migration www.xk.docker-registry.com:5501/migration:`cat migration/md5` $MIGRATION_USER $MIGRATION_PASSWORD $MIGRATION_URL 1
该过程主要包含三个步骤:
- 生成db目录下文件和的md5值,保证本次tag的唯一性,同时docker镜像的tag也使用该值,有利于后期项目维护
- 构建docker镜像并推送至私有仓库
- 由于本文是示例环境,执行docker run进行环境的全量更新
实际效果
示例环境变量配置如下:
export MIGRATION_PASSWORD="xxxxxx"
export MIGRATION_URL="jdbc:mysql://127.0.0.1:3361/liquibase_test?createDatabaseIfNotExist=true&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
export MIGRATION_USER=xxxxxx
将所有代码推送至仓库,观察结果,这块示例使用的mysql数据库。
版本升级
示例中版本升级是自动化全量升级,最终调用的是migration_all.sh脚本。查看gitlab的cicd流水线:
流水线在32秒内进行编译并执行成功,查看详细的Job详情:
查看Mysql数据库的升级情况:
新建了branch和employee两张数据库表,同时创建了liquibase的版本信息表databasechangelog和databasechangeloglock。
liquibase在databasechangelog中记录了具体的版本信息,liquibase依赖这张表对数据库的版本进行控制,表中内容包含了changeset的id、作者、文件名、执行时间、执行顺序以及tag等信息,可以看到前面介绍的的那个空的changeset在其中的描述信息就是empty,表示这个changeset什么都没干。
现在修改002_insert_data.sql,changelog在inset-branch-01的基础上新增一个changeset:insert-branch-employee-02:
--liquibase formatted sql
--changeset techgeeknext:inset-branch-01
INSERT INTO branch VALUES(1, 'User1',01);
INSERT INTO branch VALUES(2, 'User2',02);
INSERT INTO branch VALUES(3, 'User3',03);
INSERT INTO branch VALUES(4, 'User4',04);
--rollback DELETE FROM branch WHERE id in (1,2,3,4);
--changeset techgeeknext:insert-branch-employee-02
INSERT INTO employee VALUES(5, 'User5');
INSERT INTO branch VALUES(5, 'User4',05);
--rollback DELETE FROM branch WHERE id in (5);DELETE FROM employee WHERE id in (5);
提交代码,推送至代码库,查看自动化部署的结果。
查看数据,已经更新,databasechangelog中也新增了一条changeset记录:
版本回退
版本回退需要手动在后台执行docker命令。
基于tag的回退
现在存在两个tag标记, 前面的tag为84740b811c87a7c3fba5193f151a9c82,按照这个tag在服务器上执行回退命令:
docker run --net=host -v /var/logs/liquibase:/var/logs/liquibase --name migration www.xk.docker-registry.com:5501/migration:c81cd234dd5a956f0f6826728f1bd23b $MIGRATION_USER $MIGRATION_PASSWORD $MIGRATION_URL 3 84740b811c87a7c3fba5193f151a9c82
脚本执行结果:
####################################################
## _ _ _ _ ##
## | | (_) (_) | ##
## | | _ __ _ _ _ _| |__ __ _ ___ ___ ##
## | | | |/ _` | | | | | '_ \ / _` / __|/ _ \ ##
## | |___| | (_| | |_| | | |_) | (_| \__ \ __/ ##
## \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___| ##
## | | ##
## |_| ##
## ##
## Get documentation at docs.liquibase.com ##
## Get certified courses at learn.liquibase.com ##
## Free schema change activity reports at ##
## https://hub.liquibase.com ##
## ##
####################################################
Starting Liquibase at 10:57:58 (version 4.6.1 #98 built at 2021-11-04 20:16+0000)
Liquibase Version: 4.6.1
Liquibase Community 4.6.1 by Liquibase
Rolling Back Changeset:db/changelogs/migration/DML/002_insert_data.sql::insert-branch-employee-02::techgeeknext
Logs saved to /var/logs/liquibase/liquibase.log
Liquibase command 'rollback' was executed successfully.
查看数据,已经回退到指定tag为84740b811c87a7c3fba5193f151a9c82的版本:
基于changeset个数的回退
命令的最后参数表示回退的changeset个数为2,服务器上执行该命令。
docker run --net=host -v /var/logs/liquibase:/var/logs/liquibase --name migration www.xk.docker-registry.com:5501/migration:c81cd234dd5a956f0f6826728f1bd23b $MIGRATION_USER $MIGRATION_PASSWORD $MIGRATION_URL 2 2
命令执行结果:
####################################################
## _ _ _ _ ##
## | | (_) (_) | ##
## | | _ __ _ _ _ _| |__ __ _ ___ ___ ##
## | | | |/ _` | | | | | '_ \ / _` / __|/ _ \ ##
## | |___| | (_| | |_| | | |_) | (_| \__ \ __/ ##
## \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___| ##
## | | ##
## |_| ##
## ##
## Get documentation at docs.liquibase.com ##
## Get certified courses at learn.liquibase.com ##
## Free schema change activity reports at ##
## https://hub.liquibase.com ##
## ##
####################################################
Starting Liquibase at 11:08:14 (version 4.6.1 #98 built at 2021-11-04 20:16+0000)
Liquibase Version: 4.6.1
Liquibase Community 4.6.1 by Liquibase
Rolling Back Changeset:db/changelogs/migration/DML/002_insert_data.sql::inset-branch-01::techgeeknext
Rolling Back Changeset:db/changelogs/baseline/DML/001_insert_data_baseline.sql::inset-employee-01::techgeeknext
Logs saved to /var/logs/liquibase/liquibase.log
Liquibase command 'rollbackCount' was executed successfully.
查看数据,branch与employee数据均为空,databasechangelog减少了两条changeset记录,表示已经回退成功:
总结
本文主要介绍了liquibase在实际项目中的应用实践,liquibase本身的功能比较强大,本文仅使用了其中非常小的一部分,满足了我们的项目需求,更多功能可以在liquibase官网进行探索。同时,本文使用了自动化流水线的方式引入liquibase进行数据库的版本控制,开发人员仅需要添加更新的sql脚本,推送代码后gitlab的流水线会自动触发数据库版本的升级,无需人工干预,避免了低级错误。
参考资料
liquibase官方文档
如何在已有项目中引入liquibase并进行多环境的增量更新
示例代码地址
https://github.com/jy03187487/liquibase_example
需要注意的问题
- 具体数据库的依赖需要自行下载放置到liquibase/lib目录下
- .gitlab-ci.yml使用的环境变量需要提前配置在系统中,该项目直接配置到了/etc/profile中,保险起见,配置完成后请重启gitlab-runner确认环境变量已经生效
- find migration/db/ -type f | sort 添加sort是为了保证文件顺序一致,因为发现在不同操作系统中执行find migration/db/ -type f 顺序不一定一致
转载说明
码字不易,如需转载请注明出处。