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

news2024/9/25 21:09:58

多租户动态多数据源系列

1、基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表
2、基于springboot+jpa 实现多租户动态切换多数据源 - 基于dynamic-datasource实现多租户动态切换数据源
3、基于springboot+jpa 实现多租户动态切换多数据源 - 使用Flyway实现多数据源数据库脚本管理和迭代更新

目录

  • 多租户动态多数据源系列
  • 多租户理解
  • dynamic-datasource介绍
  • 多租户多数据源实现
    • pom配置
    • yaml配置
    • 项目文件结构
    • 数据源相关操作
      • 查看数据源
      • 增改数据源
        • 增加数据源
        • 改数据源
        • dataSourceProperty必要的配置参数
      • 删除数据源
      • 切换数据源
        • 注解方式@DS
        • 手动切换数据源
        • 切换数据源不生效
        • 多数据源事务
          • 解决方法:本地事务
    • 启动项目测试验证

多租户理解

多租户定义:多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。

简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。
从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。租户的数据既有隔离又有共享,从而解决数据存储的问题

多租户的三种实现方案:

1.独立数据库:即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本也高。(正如我在分库分表文章中分析,所选就是独立数据库)
2.共享数据库,独立 Schema:多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)
3.共享数据库,共享 Schema,共享数据表:这个其实就类似权限了,所有租户用一个表,通过一个id字段进行区分

dynamic-datasource介绍

原本计划参考一些网上数据源切换的实现,自己造一个🛞,但是实际需求要实现的可能更复杂,自己造轮子费时费力,于是在冲浪中找到了便捷的现有🛞:dynamic-datasource-spring-boot-starter

文档: dynamic datasource
详细付费文档(好像就十几块钱):dynamic datasource

简介

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。
其支持 Jdk 1.7+, SpringBoot 1.4.x 1.5.x 2.x.x。

特性

  • 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  • 支持数据库敏感配置信息 加密 ENC()。
  • 支持每个数据库独立初始化表结构schema和数据库database。
  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
  • 支持 自定义注解 ,需继承DS(3.2.0+)。
  • 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
  • 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
  • 提供 自定义数据源来源 方案(如全从数据库加载)。
  • 提供项目启动后 动态增加移除数据源 方案。
  • 提供Mybatis环境下的 纯读写分离 方案。
  • 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
  • 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
  • 提供 基于seata的分布式事务方案。
  • 提供 本地多数据源事务方案。 附:不能和原生spring事务混用。

使用体验: 支持较为功能功能,基于此实现了我在项目中的动态增、删、修改、切换数据源的需求,也支持解决事务问题

多租户多数据源实现

项目架构说明: 项目现有架构是springboot+jpa+maven

pom配置

<!-- https://www.kancloud.cn/tracy5546/dynamic-datasource/2394605 -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
	<version>3.5.1</version>
</dependency>

yaml配置

通过yaml配置主数据源,由于我要实现多租户的动态增删改数据源,这里就只配置了一个主数据源,后续通过代码来自由的增删数据源。

当然,如果你是确定的几个数据源,可以直接都在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 # 配置则生效,自动初始化表结构
# 如下,如果你是确定的几个数据源,可以直接都在yaml配置写死即可
#        slave_1:
#          url: 
#          username: 
#          password: 
#          driver-class-name: com.mysql.cj.jdbc.Driver

项目文件结构

在这里插入图片描述

数据源相关操作

针对数据源的增删改查以及切换操作,由于方式不同是有区别的,可以只通过yaml配置,也可以代码中动态操作,更为灵活的可以yaml配置结合代码动态组合

查看数据源

//这是官方示例,直接返回PoolName(就是yaml配置中的数据源名称)的Set
@GetMapping
@ApiOperation("获取当前所有数据源")
public Set<String> now() {
    DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
    return ds.getDataSources().keySet();
}

//如果只是想知道具体的数据源,输出查看PoolName即可,如下
public void getDataSources() {
    DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
    for (String poolName : ds.getDataSources().keySet()) {
        log.info("poolName:" + poolName);
    }
}

增改数据源

增加数据源

如果是yaml配置方式写死的数据源,那么直接在yaml配置中添加即可

我是使用了更灵活的操作,通过yaml配置主数据源,在程序中动态添加其他数据源如下
这样就可以通过用户页面操作来动态添加数据源

//通用数据源会根据maven中配置的连接池根据顺序依次选择。
//默认的顺序为druid>hikaricp>beecp>dbcp>spring basic
@PostMapping("/add")
@ApiOperation("通用添加数据源(推荐)")
public Set<String> add(@Validated @RequestBody DataSourceDTO dto) {
    DataSourceProperty dataSourceProperty = new DataSourceProperty();
    // 这里主要是将dto的属性赋值给dataSourceProperty
    //所以dataSourceProperty中必要的参数,dto都要提供
    BeanUtils.copyProperties(dto, dataSourceProperty);
    DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
    DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
    // PoolName就是我们yaml配置中说的数据源名称
    ds.addDataSource(dto.getPoolName(), dataSource);
    return ds.getDataSources().keySet();
}

改数据源

DynamicRoutingDataSource中记录数据源是以map形式,如下
在这里插入图片描述
dataSourceMap的k就是poolName,所以如果想要更改poolName对应的数据源,直接覆盖同k的value即可。或者更为保险的做法可以先删除poolName对应的数据源,后续再次添加同名poolName数据源

dataSourceProperty必要的配置参数

在这里插入图片描述

删除数据源

如果是yaml配置方式写死的数据源,那么直接在yaml配置中删除即可

通过程序动态删除数据源如下

@DeleteMapping
@ApiOperation("删除数据源")
public String remove(String poolName) {
    DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
    ds.removeDataSource(poolName);
    return "删除成功";
}

切换数据源

前面的增删改查数据源操作还是比较简单,也没有太多逻辑在,但是切换数据源就要谨慎操作了,毕竟数据落入错误的库,后果是可大可小的

注解方式@DS

dynamic-datasource中DS作为切换数据源核心注解,一般明确哪个或哪组数据源的情况下是完全符合的(⚠️:如果是分组数据源会随机调用分组内的数据源)
但动态切换数据源方式不建议采用此种,可直接跳过看下面

DS放在哪里合适?

首先开发者要了解的基础知识是,DS注解是基于AOP的原理实现的,aop的常见失效场景应清楚。 比如内部调用失效,shiro代理失效。 具体见切换数据源失败

  1. 通常建议DS放在serviceImpl的方法上,如事务注解一样。
  2. 注解在Controller的方法上或类上

并不是不可以,并不建议的原因主要是controller主要作用是参数的检验等一些基础逻辑的处理,这部分操作常常并不涉及数据库

  1. 注解在service的实现类的方法或类上

这是建议的方式,service主要是对业务的处理, 在复杂的场景涉及连续切换不同的数据库。 如果你的方法有通用性,其他service也会调用你的方法。 这样别人就不用重复处理切换数据源

  1. 注解在mapper上。

通常如果你某个Mapper对应的表只在确定的一个库,也是可以的。 但是建议只注解在Mapper的类上。

  1. 其他使用方式
    继承抽象类上的DS
  2. 继承接口上的DS

3.4.1开始支持, 但是需要注意的是,一个类能实现多个接口,如果多个接口都有DS会如何?
。。。。。。不知道,别这么干。。。。

  • 问:比如我有一个抽象Service,我想实现继承我这个抽象Service下的子Service的所有方法除非重新指定,都用我抽象Service上注解的数据源。 是否支持?
  • 答:支持。

手动切换数据源

动态数据源切换,尤其是像我这种多租户场景,同一个方法可能要根据实际情况切换对应的数据源,这时就不能使用上面DS注解方式写死数据源或者数据源组,只能手动切换

手动切换代码如下

public static void switchDataSource(String poolName) {
    //需要注意的是手动切换的数据源,最好自己在合适的位置
    //调用DynamicDataSourceContextHolder.clear()清空当前线程的数据源信息。
    DynamicDataSourceContextHolder.clear();
    //切换到对应poolName的数据源
    DynamicDataSourceContextHolder.push(poolName);
}

这样就可以根据不同租户的操作来自由切换数据源,同一个方法来回切换数据源也不必担心,这是线程级别的,不会相互影响

如果需要通过用户每次请求,拦截token或url来切换数据源,可以参考下面写一个拦截器,注册进spring里即可。

public class DynamicDatasourceClearInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
    
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        DynamicDataSourceContextHolder.clear();
    }
}

切换数据源不生效

开启了spring的事务

这里只举例我实际开发中也是遇到的问题,当然这个也是最普遍的情况

原因: spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。

请检查整个调用链路涉及的类的方法和类本身还有继承的抽象类上是否有@Transactional注解。

如强烈需要事务保证多个库同时执行成功或者失败,请查看下面多数据源事务的解决办法。

有其他不生效的情况可以私信我

多数据源事务

@Transaction开启了事务,为什么多数据源事务不生效? 简单来说:嵌套数据源的service中,如果操作了多个数据源,不能在最外层加上@Transaction开启事务,否则切换数据源不生效,因为这属于分布式事务了,需要用seata方案解决,如果是单个数据源(不需要切换数据源)可以用@Transaction开启事务,保证每个数据源自己的完整性

加事务不生效的原因:
dynamic-datasource切换数据源的原理就是实现了DataSource接口,实现了getConnection方法,只要在service中开启事务,service中对其他数据源操作只会使用开启事务的数据源,因为开启事务数据源会被缓存下来,可以在DataSourceTransactionManager的doBegin方法中看见那个txObject,如果在一个事务内,就会复用Connection,所以切换不了数据源

解决方法:本地事务

通过本地事务实现很简单,就是循环提交,发生错误,循环回滚。 我们默认的前提是数据库本身不会异常,比如宕机。
如数据在回滚的过程突然宕机,本地事务就会有问题。如果你需要完整分布式方案请使用seata方案。

使用方法
在最外层的方法添加 @DSTransactional,底下调用的各个类就正常切换数据源即可。

简单举例如下:


@DeleteMapping
//只要@DSTransactional注解下任一环节发生异常,则全局多数据源事务回滚。
@DSTransactional()
@ApiOperation("删除数据源")
public String remove(String poolName) {
    DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
    ds.removeDataSource(poolName);
    return "删除成功";
}

但一定要注意Spring事务@Transational和本地事务@DSTransactional,不能混用

启动项目测试验证

可以看到除了master数据源,我这里还有两个数据源,都是之前操作的时候加入的数据源
在这里插入图片描述
到这里,多数据源的动态切换应该就可以实现了,其他业务相关操作,可以自行补充了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/127326.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java:jackson 四:Jackson Property Inclusion Annotations

java&#xff1a;jackson 四&#xff1a;Jackson Property Inclusion Annotations 1 前言 参考文档地址&#xff1a; https://www.baeldung.com/jacksonhttps://www.baeldung.com/jackson-annotationsSpringBoot自带的jackson版本如下&#xff1a; <parent><artif…

数据可视化,21-30岁消费增速最快,年轻人正在成长为白酒消费的主力

2022中国白酒消费报告 中国酿酒的发源距今已经有四千多年的历史&#xff0c;中国有很多酒”酒乡”贵州的茅台、四川泸州的国窖、四川宜宾的五粮液。人们常说“把酒言欢”&#xff0c;这不马上就要过春节了&#xff0c;过节送礼、家庭聚会都非常适合&#xff0c;小编使用在线数据…

VSCode插件大全

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录一、必备插件1.Chinese&#xff08;中文&#xff09;2.Settings Sync&#xff08;配置同步到云端&#xff09;二、效率插件1.HTML Snippets&#xff08;代码提示&…

观察者模式(observer pattern) / 发布-订阅模式(Publish-Subscribe)

一个遍历问题导致的低效率范例 #include <iostream> #include <thread> #include <vector> #include <list> #include <mutex> #include <chrono> #include <algorithm> #include <set> #include <queue>using namespa…

一个三臂非劣效性检验的包简介——“ThreeArmedTrials”

目录0引言1.模型分类2. R语言函数介绍2.1 函数总览2.2 GElesions函数&#xff08;数据集1&#xff09;2.3 opt_alloc_RET函数2.4 power_RET函数2.5 remission&#xff08;数据集2&#xff09;2.6 seizures函数&#xff08;数据集3&#xff09;2.7 T2lesions函数&#xff08;数据…

多线程基础部分

多线程基础部分1. 线程与进程的关系1.1 多线程启动1.2 线程标识1.2.1 Thread与Runnable1.3 线程状态2.线程池入门2.1 ThreadPoolExecutor2.2 创建线程池2.3 关闭线程池创建线程的几种方法参考1. 线程与进程的关系 1个进程包含1个或多个线程。 1.1 多线程启动 线程有两种启动…

骨传导耳机是怎么传声的、骨传导耳机的优点是什么

要说这两年最火的蓝牙耳机是哪款&#xff0c;大火的骨传导耳机绝对可以名列前茅&#xff0c;那可真是运动健身、需长时佩戴耳机党的神器&#xff01;如果你是搞运动的、健身的&#xff0c;或者是需要长时间佩戴耳机上网课的学生党&#xff0c;那一副靠谱的骨传导耳机绝对是必不…

LVGL学习笔记7 - GD32平台优化

目录 1. 颜色深度 2. 更新disp_init 3. 更新disp_flush 4. 改为IPA更新数据 5. 死机问题 学习过程中发现GD32平台的显示效果不佳&#xff0c;而且会出现死机的问题&#xff0c;需要优化一下平台代码。 1. 颜色深度 修改颜色深度为32bit。 #define LV_COLOR_DEPTH 32 2.…

时序引擎架构和实例演练

一、时序引擎介绍 开务数据库时序引擎是一款功能丰富、高性能的时序引擎&#xff0c;专为物联网、工业互联网、数字能源、金融等场景设计并优化。它能让大量设备、数据采集器每天产生的高达 TB 甚至 PB 级的数据得到高效实时的处理&#xff0c;对业务的运行状态进行实时的监测、…

银行卡数据标签的列举与使用

银行卡三要素&#xff1a;银行卡号、姓名、身份证号&#xff0c;银行卡四要素是指银行卡号、姓名、身份证号、手机号。对于从事信贷风控的小伙伴来讲&#xff0c;并不陌生。 银行卡信息的应用可能更熟悉的是客户信息核验&#xff0c;也就是针对信贷客户审批额度发放之前&#x…

SpringCloud系列(七)最详细最全面详述统一网关 Gateway

有道词典上对 Gateway 有大门口, 门道, 通道以及计算机术语中的网关之意, 其实对于网关这个概念是很好理解的, 例如有这样高档的小区车库, 当开车经过闸口的时候会识别你的车牌号, 识别成功后会自动将你的车库门打开; 其实计算机中的网关也是如此, 在 Spring Cloud 中网关的实现…

【1 - 决策树 - 原理部分】菜菜sklearn机器学习

课程地址&#xff1a;《菜菜的机器学习sklearn课堂》_哔哩哔哩_bilibili 第一期&#xff1a;sklearn入门 & 决策树在sklearn中的实现第二期&#xff1a;随机森林在sklearn中的实现第三期&#xff1a;sklearn中的数据预处理和特征工程第四期&#xff1a;sklearn中的降维算法…

LOAM和SSL-SLAM

今天来水两个激光SLAM的相关框架的学习笔记。 一、LOAM 首先介绍scan-to-scan map-to-map scan-to-map之间的关系&#xff1a; 1.scan-to-scan匹配 即两帧激光雷达数据之间的匹配&#xff0c;目的是求得从起始帧A到目标帧B的相对平移量与旋转矩阵。目前来说scan-toscan中&a…

Elasticsearch搜索引擎

The Elastic Stack, 包括 Elasticsearch【搜索&#xff0c;分析】、 Kibana【可视化】、 Beats 和 Logstash【数据的搜集】&#xff08;也称为 ELK Stack&#xff09;。能够安全可靠地获取任何来源、任何格式的数据&#xff0c;然后实时地对数据进行搜索、分析和可视化。 Elati…

安装压缩包版mysql

一、mysql-8.0.21-winx64.zip解压 二、在解压后的目录下添加data目录 三、配置环境变量 win7&#xff1a; ​ 我的电脑–>属性–>高级系统设置–>高级–>环境变量 ​ 在下面系统变量中 ​ 新建 ​ 变量名&#xff1a;MYSQL_HOME ​ 变量值&#xff1a;E:\MySQL\my…

常用的接口安全性保障手段

http接口有哪些安全问题 数据被抓包窃取数据被恶意篡改数据被爬取泄漏Token授权机制 用户使用用户名密码登录后服务器给客户端返回一个Token&#xff08;通常是UUID&#xff09;&#xff0c;并将Token-UserId以键值对的形式存放在缓存服务器中。服务端接收到请求后进行Token验…

UG NX二次开发(C#)-曲线-NXOpen.Curve初探

系列文章目录 `` 例如:第一章 初探NXOpen.Curve类 文章目录 系列文章目录1.前言2.NXOpen.Curve2. NXOpen.Curve包含的子类3.曲线类型的获取4.将曲线对象转换为子类类型1.前言 介绍下NXOpen.Curve类、Curve类型的获取、一些创建曲线的封装方法(包括直线、样条曲线、圆锥曲线…

OSM数据内容解析

OSM数据内容解析 数据简介 OpenStreetMap&#xff08;简称OSM&#xff0c;中文是公开地图&#xff09;&#xff0c;这是一个网上地图协作计划&#xff0c;目标是创造一个内容自由且能让所有人编辑的世界地图。是一款由网络大众共同打造的免费开源、可编辑的地图服务。 OSM采…

成功实施APS生产排程系统,必须具备哪些条件?

在许多生产管理者眼中&#xff0c;生产作业计划是不重要的&#xff0c;如果我们只停留在小加工作坊的规模&#xff0c;大脑就能把一个月的订单、物料、资源记得清清楚楚&#xff0c;那么生产计划排程的必要性确实不太大&#xff0c;但事实上&#xff0c;随着生产规模的扩大&…

JDK1.8中HashMap的resize()方法详解

JDK1.8中HashMap的resize()方法详解 文章目录JDK1.8中HashMap的resize()方法详解[toc]一、概述二、源码解析三、元素迁移四、小结在学习本文之前&#xff0c;默认大家已经有了HashMap源码的前置知识。 「集合底层」深入浅出HashMap底层源码 一、概述 resize()方法的代码比较长…