基于springboot+jpa 实现多租户动态切换多数据源 - 使用Flyway实现多数据源数据库脚本管理和迭代更新

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 基于springboot+jpa 实现多租户动态切换多数据源 - 使用Flyway实现多数据源数据库脚本管理和迭代更新

多租户动态多数据源系列


1、基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表

2、基于springboot+jpa 实现多租户动态切换多数据源 - 基于dynamic-datasource实现多租户动态切换数据源

3、基于springboot+jpa 实现多租户动态切换多数据源 - 使用Flyway实现多数据源数据库脚本管理和迭代更新


前言


自从项目变成了多数据源架构,不同数据源的数据库表结构还不同,JPA的自动生成表结构功能已经不能再使用了,需要改为原生sql来支持多数据源。

每次服务的代码更新部署,难免会存在数据库结构的变更以及字典数据的添加,手动执行更新脚本是一个耗时耗力的工作,而且还会出现遗漏或者其他状况,这时急需一个自动执行数据库脚本的工具来解决问题


为此我做了相关调研,发现常见的开源迁移工具 Liquibase 和 Flyway使用和对比最多,那么就进行一场二选一的角逐吧!


Liquibase 还是 Flyway


8e48003891b74b539a7187dff706ddd1.png



首先快速概括下这两个工具:


Flyway 和 Liquibase都支持专业数据库重构和版本控制所需的所有功能,因此您将始终知道要处理的数据库模式的版本以及它是否与软件版本匹配。两种工具都集成在 Maven或 Gradle 构建脚本中以及 Spring Boot 生态系统中,因此您可以完全自动化数据库重构。


Flyway 使用 SQL或Java 定义数据库更改,因此您可以定制 SQL脚本,使其与基础数据库技术(例如Mysql、Oracle、PostgreSQL等)良好地配合使用。

Liquibase 使用 XML,YAML 或 JSON格式 来定义数据库更改来引入抽象层。因此,Liquibase 更适合在具有不同基础数据库技术的不同环境中安装的软件产品中使用。


image.png

image.png


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脚本,而不会相互影响到


a17fefab95ea41fa9b44c3a083c7d214.png

对应文件夹下添加需要运行的sql脚本


sql脚本的命名规范为:V+版本号(版本号的数字间以”.“或”_“分隔开)+双下划线(用来分隔版本号和描述)+文件描述+后缀名,例如:V1.0.1__update_org_config_db_table.sql。

具体可参考上下文


基于SQL迁移的文件命名规则


常用的迁移方式就是基于sql的迁移,如上图,我就是采用的sql迁移方式,但是文件命名有一定的规则

9d0572eb3aad4008bd349db9f8f6765a.png

文件名由如下几部分组成:


前缀: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();
            }
        });
    }
}

启动项目

启动日志验证


a7e55b0295f04fcdab525d9486880aba.png


数据库验证

在数据库中可以看到已按照定义好的脚本,完成数据库变更,并在flyway_schema_history表插入了sql执行记录:

e50b44cd34c740468956f9a494bbda85.png


如果上述验证都没问题了,那就是没有问题了


注意事项


下面的注意事项一定要认真仔细的阅读,不然可能就删库跑路了


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执行失败时,元数据表会有错误记录

8b231efa2dd04d2abb8fce65adbe4094.png


1.查看错误日志及原因


在console 中查看错误日志及原因,如下图:

有问题的脚本:V1.0.3__creat_table.sql

具体错误信息:Table ‘df_org_config_test_001’ already exists

5f3a4e7d85c645589a0c30b1091449f9.png

2.根据错误信息,修复SQL脚本

我的具体错误信息是已经存在了表df_org_config_test_001,那么我修改为创建表df_org_config_test_002

3.删除元数据表失败记录

在数据库中打开 df_flyway_schema_history_test 表,删除 success 字段为 0 (0状态为失败,1状态为成功) 的记录(表中最后一条记录)


4.再次启动项目即可

一般来说这样就解决了问题,如果还是启动不成功,那就要再仔细检查一下脚本了


多数据源系列目前就告一段落了!!!!

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
21天前
|
Java 数据库连接 测试技术
SpringBoot入门 - 添加内存数据库H2
SpringBoot入门 - 添加内存数据库H2
32 3
SpringBoot入门 - 添加内存数据库H2
|
17天前
|
Java 关系型数据库 数据库连接
使用 Spring Boot 执行数据库操作:全面指南
使用 Spring Boot 执行数据库操作:全面指南
56 1
|
1月前
|
Java 数据库连接
SpringBoot配置多数据源实战
第四届光学与机器视觉国际学术会议(ICOMV 2025) 2025 4th International Conference on Optics and Machine Vision
56 8
|
1月前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
66 13
|
1月前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
43 4
|
1月前
|
存储 安全 Java
springboot当中ConfigurationProperties注解作用跟数据库存入有啥区别
`@ConfigurationProperties`注解和数据库存储配置信息各有优劣,适用于不同的应用场景。`@ConfigurationProperties`提供了类型安全和模块化的配置管理方式,适合静态和简单配置。而数据库存储配置信息提供了动态更新和集中管理的能力,适合需要频繁变化和集中管理的配置需求。在实际项目中,可以根据具体需求选择合适的配置管理方式,或者结合使用这两种方式,实现灵活高效的配置管理。
19 0
|
6天前
|
关系型数据库 MySQL 数据库
Python处理数据库:MySQL与SQLite详解 | python小知识
本文详细介绍了如何使用Python操作MySQL和SQLite数据库,包括安装必要的库、连接数据库、执行增删改查等基本操作,适合初学者快速上手。
58 15
|
7天前
|
关系型数据库 MySQL 数据库
数据库数据恢复—MYSQL数据库文件损坏的数据恢复案例
mysql数据库文件ibdata1、MYI、MYD损坏。 故障表现:1、数据库无法进行查询等操作;2、使用mysqlcheck和myisamchk无法修复数据库。
|
11天前
|
SQL 关系型数据库 MySQL
MySQL导入.sql文件后数据库乱码问题
本文分析了导入.sql文件后数据库备注出现乱码的原因,包括字符集不匹配、备注内容编码问题及MySQL版本或配置问题,并提供了详细的解决步骤,如检查和统一字符集设置、修改客户端连接方式、检查MySQL配置等,确保导入过程顺利。
|
19天前
|
关系型数据库 MySQL 数据库
GBase 数据库如何像MYSQL一样存放多行数据
GBase 数据库如何像MYSQL一样存放多行数据