多租户动态多数据源系列
1、基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表
2、基于springboot+jpa 实现多租户动态切换多数据源 - 基于dynamic-datasource实现多租户动态切换数据源
3、基于springboot+jpa 实现多租户动态切换多数据源 - 使用Flyway实现多数据源数据库脚本管理和迭代更新
前言
自从项目变成了多数据源架构,不同数据源的数据库表结构还不同,JPA的自动生成表结构功能已经不能再使用了,需要改为原生sql来支持多数据源。
每次服务的代码更新部署,难免会存在数据库结构的变更以及字典数据的添加,手动执行更新脚本是一个耗时耗力的工作,而且还会出现遗漏或者其他状况,这时急需一个自动执行数据库脚本的工具来解决问题
为此我做了相关调研,发现常见的开源迁移工具 Liquibase 和 Flyway使用和对比最多,那么就进行一场二选一的角逐吧!
Liquibase 还是 Flyway
首先快速概括下这两个工具:
Flyway 和 Liquibase都支持专业数据库重构和版本控制所需的所有功能,因此您将始终知道要处理的数据库模式的版本以及它是否与软件版本匹配。两种工具都集成在 Maven或 Gradle 构建脚本中以及 Spring Boot 生态系统中,因此您可以完全自动化数据库重构。
Flyway 使用 SQL或Java 定义数据库更改,因此您可以定制 SQL脚本,使其与基础数据库技术(例如Mysql、Oracle、PostgreSQL等)良好地配合使用。
Liquibase 使用 XML,YAML 或 JSON格式 来定义数据库更改来引入抽象层。因此,Liquibase 更适合在具有不同基础数据库技术的不同环境中安装的软件产品中使用。
Flyway
实现数据库变更原理:
项目启动时拉起Flyway,先检查数据库里面有没有Flyway元数据表,没有则创建,在数据库表中默认新建一个数据表用于存储flyway的运行信息,默认表名:flyway_schema_history;
检查 classpath 中所有的变更;
对比变更和自己的表,如果变更的版本低于或等于当前版本,不做任何变动;否则,变更会按从低到高排序,并依次执行;
执行完,在 元数据表 做相应的记录
Liquibase
实现数据库变更原理:
默认情况下,Bean会在/db/changelog(相对于Classpath根目录)里查找db.changelog-master.yaml文件。Liquibase变更集都集中在一个文件里。
changeset命令后的那行有一个id属性,要对数据库进行后续变更。可以添加一个新的changeset,只要id不一样就行。此外,id属性也不一定是数字,可以包含任意内容。
应用程序启动时,Liquibase会读取db.changelog-master.yaml里的变更集指令集,与之前写入databaseChangeLog表里的内容做对比,随后执行未运行过的变更集。
对比
两者的基本功能其实都差不多
都是 Java 开发的开源数据库变更管理工具
支持大部分的数据库
和 Maven/Gradle 无缝集成
和 Spring 无缝集成
非常类似的变更实现方式
复杂变更如果 SQL 不能满足的话,都可以用 Java 代码实现
较大区别是 Flyway 的变更以纯 SQL 为脚本,简单直接;Liquibase 比较厚重,当然花样也比较多,包括:
可指定不同的 profile
具有通用的变更操作支持不同的数据库,如 createTable
Liquibase 开源版本支持 diff 模式,而此特性 Flyway 必须用商业版
Liquibase 开源版本支持回滚 rollback,而此特性 Flyway 必须用商业版,Liquibase 的付费版本据说对不同种类的回滚有更复杂的支持。
两者指定变更执行顺序的方法不同,Flyway 通过固定的文件名格式来确定顺序,而 Liquibase 就是通过给定文件的顺序来执行,所以开发人员还要遵守好命名规则,例如按照日期/时间顺序命名
结论
如果想完全控制 SQL,Flyway 是首选工具,因为可以使用完全定制的 SQL 甚至 Java 代码来更改数据库。
多种数据库类型的数据源的情况下使用 Liquibase 会更加合适,不需要维护多种数据库脚本,和学习多种数据库语言,Liquibase 对于大型项目更加友好。
综上所述,我在项目中选择了 Flyway。原因有二:
项目是通过SQL脚本构建库表结构
虽然是多数据源,但数据库类型只有Mysql
项目结合Flyway实现数据库迭代更新
pom配置
<!-- flyway --> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> <version>7.10.0</version> </dependency>
yaml配置
#mysql environment spring: datasource: dynamic: hikari: connection-timeout: 5000 idle-timeout: 30000 # 经过idle-timeout时间如果连接还处于空闲状态, 该连接会被回收 min-idle: 5 # 池中维护的最小空闲连接数, 默认为 10 个 max-pool-size: 16 # 池中最大连接数, 包括闲置和使用中的连接, 默认为 10 个 max-lifetime: 60000 # 如果一个连接超过了时长,且没有被使用, 连接会被回收 is-auto-commit: true primary: master #设置默认的数据源或者数据源组,默认值即为master strict: true #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源 datasource: master: url: username: password: driver-class-name: com.mysql.cj.jdbc.Driver init: schema: db/primary_db_table.sql # 上面是多数据源配置,下面开始才是flyway配置 flyway: # 是否启用flyway enabled: true # metadata 版本控制信息表 默认 flyway_schema_history table: flyway_schema_history_test # 如果没有元数据表,在执行flyway migrate命令之前, 必须先执行flyway baseline命令 # 非空数据库初始化Flyway时需要打开此开关进行Baseline操作 baseline-on-migrate: true # 执行时标记的tag 默认为<<Flyway Baseline>> baseline-description: <<Flyway Baseline>> # 是否可以无序执行 开发环境建议 true 生产环境建议 false out-of-order: false # 执行迁移时是否自动调用验证SQL文件是否存在问题,当你的版本不符合逻辑时会抛出异常 validate-on-migrate: true # SQL 脚本的目录 默认值 classpath:db/migration # 这里写项目启动时的主库sql变更版本路径,然后在配置类中根据不同的数据源转换即可 locations: classpath:db/primary
根据locations配置创建文件夹
根据在yaml配置文件的脚本存放路径的配置
在resource目录下建立文件夹db/primary、db/migration
项目文件结构
可以看到我这里classpath:db/路径下有primary 和 migration两个文件夹,这里就为了不同的数据库表结构可以执行不同的sql脚本,而不会相互影响到
对应文件夹下添加需要运行的sql脚本
sql脚本的命名规范为:V+版本号(版本号的数字间以”.“或”_“分隔开)+双下划线(用来分隔版本号和描述)+文件描述+后缀名,例如:V1.0.1__update_org_config_db_table.sql。
具体可参考上下文
基于SQL迁移的文件命名规则
常用的迁移方式就是基于sql的迁移,如上图,我就是采用的sql迁移方式,但是文件命名有一定的规则
文件名由如下几部分组成:
前缀:V表示版本化,U表示撤销回退,R表示可重复迁移
版本:版本一般为数字,多个数字之间用点分割,比如:V1.1,V1等
分隔符:__双下划线 ,这里需要特别注意,是双下划线
描述:表示当前脚本执行的操作类型
后缀:.sql
上面的这些组成部分都是可以进行配置的,更多的配置可以参考官方文档:Flyway Documentation > Flyway CLI and API > Configuration
Configuration旧文档,感觉更好理解点
Flyway配置类
结合yaml配置和项目文件结构图理解,其实本质就是将application文件的flyway的locations设置为db/primary主库的路径,让主库只去执行primary文件夹下的sql文件,然后在配置类中拆解locations,根据不同的数据源重新组成对应的locations路径。
配置类的代码如下:
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.flywaydb.core.Flyway; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.annotation.PostConstruct; import javax.sql.DataSource; import java.util.Map; @Slf4j @Configuration @RequiredArgsConstructor @EnableTransactionManagement public class FlywayConfig { private final DataSource dataSource; @Value("${spring.flyway.locations}") private String SQL_LOCATION; @Value("${spring.flyway.table}") private String VERSION_TABLE; @Value("${spring.flyway.baseline-on-migrate}") private boolean BASELINE_ON_MIGRATE; @Value("${spring.flyway.out-of-order}") private boolean OUT_OF_ORDER; @Value("${spring.flyway.validate-on-migrate}") private boolean VALIDATE_ON_MIGRATE; @Bean @PostConstruct public void migrateOrder() { log.info("调用数据库生成工具"); DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; Map<String, DataSource> dataSources = ds.getDataSources(); dataSources.forEach((k, v) -> { log.info("正在执行多数据源生成数据库文件: " + k); if (k.equals("master")) { log.info("正在执行后台库数据源生成数据库文件"); // 将路径转换 SQL_LOCATION = SQL_LOCATION.split("/")[0] + "/primary"; Flyway flyway = Flyway.configure() .dataSource(v) .locations(SQL_LOCATION) .baselineOnMigrate(BASELINE_ON_MIGRATE) .table(VERSION_TABLE) .outOfOrder(OUT_OF_ORDER) .validateOnMigrate(VALIDATE_ON_MIGRATE) .load(); flyway.migrate(); } else { log.info("正在执行多机构数据源生成数据库文件"); // 将路径转换 SQL_LOCATION = SQL_LOCATION.split("/")[0] + "/migration"; Flyway flyway = Flyway.configure() .dataSource(v) .locations(SQL_LOCATION) .baselineOnMigrate(BASELINE_ON_MIGRATE) .table(VERSION_TABLE) .outOfOrder(OUT_OF_ORDER) .validateOnMigrate(VALIDATE_ON_MIGRATE) .load(); flyway.migrate(); } }); } }
启动项目
启动日志验证
数据库验证
在数据库中可以看到已按照定义好的脚本,完成数据库变更,并在flyway_schema_history表插入了sql执行记录:
如果上述验证都没问题了,那就是没有问题了
注意事项
下面的注意事项一定要认真仔细的阅读,不然可能就删库跑路了
1.严格遵守脚本命名规则
V+版本号+双下划线+脚本变更描述+后缀
例如:V1.1.0__create_table.sql
flyway.sql-migration-prefix配置前缀,默认V
flyway.sql-migration-separator配置分隔符,默认双下划线
flyway.sql-migration-suffix配置脚本后缀,默认.sql
特别注意:V1__.sql == V1.0__.sql V1.1__.sql == V1.1.0__.sql
所以配置时,最好设置显示递增的版本号,否则会报错
2.脚本文件版本号必须>基线版本号
文件的版本号必须 > 基线初始版本号,否则不会执行你的脚本
比如基线默认版本号为1,所以你的脚本版本号必须大于1,例如V1.1****
例如文件为:V1.0.0__create_table.sql,你会发现该脚本并不会执行
3.禁止删除或修改已执行的 SQL 文件
如果项目已经执行了过了某个脚本,那么这个脚本不能删除,也不能修改,否则在项目启动时会报错
删除了则是找不到以前执行的文件
修改了则是在对比checksum时报不一致
如果执行的sql脚本有问题,第一次没有跑成功,重新跑时,要么重新定义脚本的版本号,要么删除表schema_version的当前版本记录
4.脚本的版本号应严格不同
两个脚本的版本号应该严格不同,不能出现1中的特别注意项
特别注意:V1__.sql == V1.0__.sql V1.1__.sql == V1.1.0__.sql
所以配置时,最好设置显示递增的版本号,否则会报错
5.⚠️慎用地雷配置项
flyway.clean-on-validation-error:这个配置项一定要小心了
如果配置为true,当你的sql脚本执行失败时,就会执行删除库中所有表的操作,即之前的clean操作,所以一定要慎重,慎重,慎重!!!
6.Druid 与 Flyway 的冲突
Flyway通过 SQL 脚本来执行数据库的建立与更新。当同时集成了 Druid 和 Flyway 之后,Druid 的 wall防火墙极可能直接干预 SQL 脚本的操作,继而导致 Flyway 执行中断。在项目开发的过程中,配置以下防火墙属性以放行 Flyway 的SQL 操作
spring: datasource: druid: wall: config: variantCheck: false noneBaseStatementAllow: true commentAllow: true multiStatementAllow: true
7.DDL 与 DML 语句不能写在同一 SQL 文件
8.禁止提交执行失败的 SQL 文件
执行失败的 SQL 文件一定要成功解决,然后确保application 启动成功后,再提交代码到远程。
脚本执行失败排查及修复
由于Flyway导致application执行失败时,元数据表会有错误记录
1.查看错误日志及原因
在console 中查看错误日志及原因,如下图:
有问题的脚本:V1.0.3__creat_table.sql
具体错误信息:Table ‘df_org_config_test_001’ already exists
2.根据错误信息,修复SQL脚本
我的具体错误信息是已经存在了表df_org_config_test_001,那么我修改为创建表df_org_config_test_002
3.删除元数据表失败记录
在数据库中打开 df_flyway_schema_history_test 表,删除 success 字段为 0 (0状态为失败,1状态为成功) 的记录(表中最后一条记录)
4.再次启动项目即可
一般来说这样就解决了问题,如果还是启动不成功,那就要再仔细检查一下脚本了
多数据源系列目前就告一段落了!!!!