根据租户id切换数据源

news2024/11/18 1:50:07

花了半天时间,使用spring-boot实现动态数据源,切换自如

  在一个项目中使用多个数据源的情况很多,所以动态切换数据源是项目中标配的功能,当然网上有相关的依赖可以使用,比如动态数据源,其依赖为,

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>3.5.1</version>
</dependency>

  今天,不使用现成的API,手动实现一个动态数据源。

一、环境及依赖

springboot、mybatis-plus的基础上实现动态数据源切换,

springboot:2.3.3.RELEASE

mybatis-plus-boot-starter:3.5.0

mysql驱动:8.0.32

除了这些依赖外没有其他的,目标是动态切换数据源。

二、实现思路

  先来看下,单数据源的情况。

  在使用springboot和mybatis-plus时,我们没有配置数据源(DataSource),只配置了数据库相关的信息,便可以连接数据库进行数据库的操作,这是为什么呐。其实是基于spring-boot的自动配置,也就是autoConfiguration,在自动配置下有DataSourceAutoConfiguration类,该类会生成一个数据源并注入到spring的容器中,这样就可以使用该数据源提供的连接,访问数据库了。

  感兴趣的小伙伴可以了解下这个类的具体实现逻辑。

  要实现多数据源,并且可以自动切换。那么肯定就不能再使用DataSourceAutoConfigurtation了,因为它只能产生一个数据源,多个数据源要怎么办,spring提供了AbstractRoutingDataSource类,该类是一个抽象类,仅有一个抽象方法需要实现

Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context.
Allows for arbitrary keys. The returned key needs to match the stored lookup key type, 
as resolved by the resolveSpecifiedLookupKey method.
@Nullable
protected abstract Object determineCurrentLookupKey();

可以根据该类实现一个动态数据源。好了,现在了解了实现思路,开始实现一个动态数据源,要做以下的准备工作。

1、配置文件;

2、自定义动态数据源;

2.1、配置文件

由于是多数据源,那么在配置文件中肯定是多个配置,不能再是一个数据库的配置了,这里使用两个mysql的配置进行演示,

#master 默认数据源
spring:
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
      username: root
      password: 123456
#slave 从数据源
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
      username: root
      password: 123456

这里使用了一个master一个slave两个数据源配置,其地址是一致的,但数据库示例不一样。 有了数据源的信息下一步要实现自己的数据源,

2.2、自定义动态数据源

  前边说,spring提供了AbstractRoutingDataSource类可以实现动态数据源,看下实现。

DynamicDatasource.java

package com.wcj.my.config.dynamic.source;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 动态数据源
 * @date 2023/6/8 19:18
 */
public class DynamicDatasource extends AbstractRoutingDataSource {
    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * <p>Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDatasourceHolder.getDataSource();
    }
}

这里的determineCurrentLookupKey方法,需要返回一个数据源,也就是说返回一个数据源的映射,这里返回一个DynamicDatasourceHolder.getDataSource()方法的返回值,DynamicDatasourceHolder是一个保存多个数据源的地方,

DynamicDatasourceHolder.java

package com.wcj.my.config.dynamic.source;

import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;

/**
 * @date 2023/6/8 19:42
 */
public class DynamicDatasourceHolder {
    //保存数据源的映射
    private static Queue<String> queue = new ArrayBlockingQueue<String>(1);
    public static String getDataSource() {
        return queue.peek();
    }
    public static void setDataSource(String dataSourceKey) {
        queue.add(dataSourceKey);
    }
    public static void removeDataSource(String dataSourceKey) {
        queue.remove(dataSourceKey);
    }
}

该类很简单,使用一个队列保存数据源的映射,提供获取/设置数据源的方法。

这里使用ThreadLocal类更合适,这样可以实现线程的隔离,一个请求会有一个线程来处理,保证每隔线程使用的数据源是一样的。

到现在为止依旧没有出现如何创建多数据源,下面就来了,不着急。

DynamicDatasourceConfig.java

package com.wcj.my.config.dynamic.source;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @date 2023/6/8 19:51
 */
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class DynamicDatasourceConfig {
    @Bean("master")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDatasource(){
        return DataSourceBuilder.create().build();
    }
    @Bean("slave")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDatasource(){
        return DataSourceBuilder.create().build();
    }
    @Bean
    @Primary
    public DataSource dataSource(){
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put("master", masterDatasource());
        dataSourceMap.put("slave", slaveDatasource());

        DynamicDatasource dynamicDatasource=new DynamicDatasource();
        dynamicDatasource.setTargetDataSources(dataSourceMap);
        dynamicDatasource.setDefaultTargetDataSource(masterDatasource());
        return dynamicDatasource;
    }
}

首先,在该类上有个一个@Configuration注解,标明这是一个配置类;

其次,有一个@EnableAutonConfiguration注解,该注解中有个数组类型的exclude属性,排除不需要自动配置的类,这里排除的是当然就是DataSourceAutoConfiguration类了;因为下面会自动生成数据源,不需要自动配置了;

然后,在类中是标有@Bean的方法,这些方法便是生成数据源类,且映射为”master“、”slave“,可以有多个。使用的是DataSourceBuilder类帮助生成;

最后,生成一个DynamicDatasource,且标有@Primary注解,这里需要设置”master“、”slave“两个映射代表的数据源;

这样便向spring容器中注入了三个数据源,分别是”master“、”slave“代表的数据源,他们是需要实际使用的数据源。还有一个是DynamicDatasource,提供数据源的设置。这三个都是DataSource的子类。

三、使用多数据源

  上面已经完成了多数据源的配置,下面看怎么使用吧,还记得DynamicDatasourceHolder类中有set/get方法吗,就是使用这个类提供的方法,

UserSerivce.java

package com.wcj.my.service;

import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
import com.wcj.my.dto.UserDto;
import com.wcj.my.entity.User;
import com.wcj.my.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @date 2023/6/8 15:19
 */
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    /**默认使用master数据源
    */
    public boolean saveUser(UserDto userDto) {

        User user = new User();
        user.setUName(userDto.getName());
        user.setUCode(userDto.getCode());
        user.setUAge(userDto.getAge());
        user.setUAddress(userDto.getAddress());
        int num = userMapper.insert(user);
        if (num > 0) {
            return true;
        }
        return false;
    }
    /**
     *使用slave数据源
     */
    public boolean saveUserSlave(UserDto userDto) {
        DynamicDatasourceHolder.setDataSource("slave");
        User user = new User();
        user.setUName(userDto.getName());
        user.setUCode(userDto.getCode());
        user.setUAge(userDto.getAge());
        user.setUAddress(userDto.getAddress());
        int num = userMapper.insert(user);
        DynamicDatasourceHolder.removeDataSource("slave");
        if (num > 0) {
            return true;
        }
        return false;
    }
}

  上面的service层方法在调用dao层方法的时候,使用DynamicDatasourceHolder.setDataSource()方法设置了需要使用的数据源, 通过这样的方式便可以实现动态数据源了。

  不知道,小伙伴们有没有感觉到,这样每次在调用方法的时候都需要设置数据源是不是很麻烦,有没有一种更方面的方式,比如说注解。

四、动态数据源注解@DDS

   现在来实现一个动态数据源的注解来代替上面的每次都调用DynamicDatasourceHolder.setDataSource()方法来设置数据源。

  先看下,@DDS注解的定义

DDS.java

package com.wcj.my.config.dynamic.source.aspect;

import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**动态数据源的注解
 * 用在类和方法上,方法上的优先级大于类上的
 * 默认值是master
 * @date 2023/6/9 16:19
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface DDS {
    String value() default "master";
}

注解@DDS使用在类和方法上,切方法上的优先级大于类上的。有一个value的属性,指明使用的数据源,默认是”master“。

实现一个切面,来切@DDS注解

 DynamicDatasourceAspect.java

package com.wcj.my.config.dynamic.source.aspect;

import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
 * 动态数据源切面
 * @date 2023/6/9 16:23
 */
@Aspect
@Component
public class DynamicDatasourceAspect {
    /**
     * 切点,切的是带有@DDS的注解
     */
    @Pointcut("@annotation(com.wcj.my.config.dynamic.source.aspect.DDS)")
    public void dynamicDatasourcePointcut(){

    }
    /**
     * 环绕通知
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("dynamicDatasourcePointcut()")
    public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
        String datasourceKey="master";

        //类上的注解
        Class<?> targetClass=joinPoint.getTarget().getClass();
        DDS annotation=targetClass.getAnnotation(DDS.class);

        //方法上的注解
        MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature();
        DDS annotationMethod=methodSignature.getMethod().getAnnotation(DDS.class);
        if(Objects.nonNull(annotationMethod)){
            datasourceKey=annotationMethod.value();
        }else{
            datasourceKey=annotation.value();
        }
        //设置数据源
        DynamicDatasourceHolder.setDataSource(datasourceKey);
        try{
           return joinPoint.proceed();
        }finally {
            DynamicDatasourceHolder.removeDataSource(datasourceKey);
        }
    }
}

 这样一个动态数据源的注解便可以了,看下怎么使用,

UserServiceByAnnotation.java

package com.wcj.my.service;

import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
import com.wcj.my.config.dynamic.source.aspect.DDS;
import com.wcj.my.dto.UserDto;
import com.wcj.my.entity.User;
import com.wcj.my.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @date 2023/6/8 15:19
 */
@Service
public class UserServiceByAnnotation {
    @Autowired
    private UserMapper userMapper;
    @DDS("master")
    public boolean saveUser(UserDto userDto){
        User user=new User();
        user.setUName(userDto.getName());
        user.setUCode(userDto.getCode());
        user.setUAge(userDto.getAge());
        user.setUAddress(userDto.getAddress());
        int num=userMapper.insert(user);
        if(num>0){
            return true;
        }
        return false;
    }
    @DDS("slave")
    public boolean saveUserSlave(UserDto userDto){
        User user=new User();
        user.setUName(userDto.getName());
        user.setUCode(userDto.getCode());
        user.setUAge(userDto.getAge());
        user.setUAddress(userDto.getAddress());
        int num=userMapper.insert(user);
        if(num>0){
            return true;
        }
        return false;
    }
}

使用起来很简单,在需要切换数据源的方法或类上使用@DDS注解即可,使用value来改变数据源就好了。

五、动态数据源的原理

  很多小伙伴可能和我有一样的疑惑,使用DynamicDatasourceHolder.setDataSource或@DDS就可以设置数据源了,是怎么实现的,下面分析下,我们指定dao层的Mapper其实是一个代理对象,其会使用mybatis中的sqlSessionTempalte进行数据库的操作,在sqlSessionTemplate中会使用DefaultSqlSession对象,最终会使用DataSource,而使用了动态数据源的对象中会注入一个DynamicDataSource,在进行数据库操作时最终会获得一个数据库连接,这里便会使用DynamicDataSource获得一个连接,由于它继承了AbstractRoutingDataSource类,看下其getConnection方法,

@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

看下determineTargetDataSource()方法,

protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");、
          //自己实现的,在调用方法时进行了设置,实现动态数据源的目的    
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

看上面的注释,determineCurrentLookupkey()方法便是在DynamicDatasource类中进行了实现,从而实现了动态设置数据源的目的。

六、总结 

本文动手实现了一个动态数据源,并切提供了注解的方式,主要有以下几点

1、继承AbstractRoutingDataSource类的determineCurrentLookupkey()方法,动态设置数据源;

2、取消DataSourceAutoConfiguration的自动配置,手动向spring容器中注入多个数据源;

3、基于@DDS注解动态设置数据源;

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

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

相关文章

JS-10-es6常用知识-对象扩展

目录 1 Object.assign&#xff1a;实现拷贝继承 2 扩展运算符(...) 1&#xff09;介绍 2&#xff09;数组中的扩展运算符 3&#xff09;对象中的扩展运算符 1 Object.assign&#xff1a;实现拷贝继承 1&#xff09;目的&#xff1a;Object.assign()方法在 JavaScript 中被…

vscode 搜索框乱码

vscode 搜索文件夹 搜索txt文件 ignore取消 搜索中文乱码 https://zhuanlan.zhihu.com/p/661347670 文件 -》首选项-》设置 搜索encoding -》设置 simpified chinese 中文插件

LabVIEW开发EOL功能测试系统

LabVIEW开发EOL功能测试系统 介绍了一种基于LabVIEW开发的EOL功能测试系统方案&#xff0c;涵盖软件架构、工作流程、模块化设计、低耦合性、易于修改与维护、稳定性及硬件选型。系统通过高效的CAN通信实现对电机控制器的全面测试&#xff0c;确保运行可靠并支持未来的升级需求…

VM-Import 导入 Debian 12 系统

介绍 之前介绍过使用 VM-Import 导入 Windows 系统到 AWS 环境启动 EC2 实例, 本文将介绍如何导入 Debian 12 系统. 本地虚拟化使用 VMWare Workstation 创建虚拟机安装和准备 Debian 12 系统, 导出 OVA 文件后上传到 S3 存储桶中再使用 AWSCLI 执行 VM-Import 命令实现导入过…

线性代数|机器学习-P7SVD奇异值分解

文章目录 1. 奇异值分解1.1 SVD求解1.2 行基和列基转换 2. Ax图像表示3. 极坐标表示4. 小结 1. 奇异值分解 现在我们用的是一个m行n列的矩阵A&#xff0c;那么我们计算下特征值方程&#xff1a; A m n x n 1 λ x n 1 ; b m 1 A m n x n 1 \begin{equation} A_{m\tim…

攻防世界---misc---misc_pic_again

1、题目描述&#xff0c;flag为hctf{}格式&#xff0c;下载附件是一张图片 2、将图片放在winhex中分析&#xff0c;没有发现奇怪的信息&#xff0c;接着将图片用Stegsolve分析&#xff0c;查看通道没有发现奇怪的图片&#xff0c;接着分析&#xff0c;对数据进行提取 3、将三个…

性能工具之 JMeter 常用组件介绍(三)

文章目录 一、常用组件介绍二、Sampler&#xff1a;取样器三、Controller:控制器&#xff08;逻辑控制器&#xff09;四、Pre Processor:预处理五、Post Processor:请求之后的处理六、Assertions:断言七、Timer:定时器八、Test Fragment&#xff1a;片段九、Config Element:配置…

【全开源】云调查考试问卷系统(FastAdmin+ThinkPHP+Uniapp)

便捷、高效的在线调研与考试新选择​ 云调查考试问卷是一款基于FastAdminThinkPHPUniapp开发的问卷调查考试软件&#xff0c;可以自由让每一个用户自由发起调查问卷、考试问卷。发布的问卷允许控制问卷的搜集、回答等各个环节的设置&#xff0c;同时支持系统模板问卷&#xff…

【CMake】CMake从入门到实战系列(十五)—— CMake中添加编译选项的几种方法

文章目录 一、前言二、add_compile_options【1】基本语法【2】参数含义【3】示例【4】备注 三、target_compile_options【1】基本语法【2】参数含义【3】示例【4】备注 四、CMAKE_C_FLAGS 或 CMAKE_CXX_FLAGS 一、前言 在嵌入式工作开发调试过程中&#xff0c;我们常会遇到需要…

科技赋能,无障碍出行的新纪元

在现代社会&#xff0c;公共设施的建设不仅是衡量城市文明程度的标尺&#xff0c;更是实现社会公平与包容的重要载体。对于盲人群体而言&#xff0c;一个完善的公共设施网络&#xff0c;意味着他们能够更加独立、自信地融入社会&#xff0c;享受与视力健全者同等的公共服务与便…

【C语言之排序】-------六大排序

作者主页&#xff1a;作者主页 数据结构专栏&#xff1a;数据结构 创作时间 &#xff1a;2024年5月18日 前言&#xff1a; 今天我们就给大家带来几种排序的讲解&#xff0c;包括冒泡排序&#xff0c;插入排序&#xff0c;希尔排序&#xff0c;选择排序&#xff0c;堆排序&…

jmeter的infludb+grafana实时监控平台

目的&#xff1a;可以实时查看到jmeter拷机信息 框架&#xff1a;将 Jmeter 的数据导入 InfluxDB &#xff0c;再用 Grafana 从 InfluxDB 中获取数据并以特定的模板进行展示 性能监控平台部署实践 一、influxDB 官网&#xff1a;https://www.influxdata.com/downloads/ wget h…

Unity 自定义房间布局系统 设计与实现一个灵活的房间放置系统 ——物体占用的区域及放置点自动化

放置物体功能 效果&#xff1a; 功能&#xff1a; 自定义物体占用区域的大小一键调整占用区域调整旋转度数&#xff0c;分四个挡位&#xff1a; NoRotation&#xff1a;该物体不能调整旋转。MaximumAngle&#xff1a;每次转动90。NormalAngle&#xff1a;每次转动45&#xff…

Vue03-HelloWord

一、Hello World 1-1、示例1 1、现有html容器&#xff1b; 2、再有vue实例。 new Vue({});中的{}是配置对象。配置对象是&#xff1a;key&#xff1a;value的格式。 el&#xff1a;element元素。id对应#&#xff0c;class对应. 把容器中变化的数据&#xff0c;交给Vue实例去保…

嵌入式软件跳槽求指导?

嵌入式软件行业的跳槽确实需要一些特定的策略和技巧。我这里有一套嵌入式入门教程&#xff0c;不仅包含了详细的视频讲解&#xff0c;项目实战。如果你渴望学习嵌入式&#xff0c;不妨点个关注&#xff0c;给个评论222&#xff0c;私信22&#xff0c;我在后台发给你。 因为这个…

[Algorithm][动态规划][两个数组的DP][最长公共子序列][不相交的线][不同的子序列][通配符匹配]详细讲解

目录 1.最长公共子序列1.题目链接2.算法原理详解3.代码实现 2.不相交的线1.题目链接2.算法原理详解3.代码实现 3.不同的子序列1.题目链接2.算法原理详解3.代码实现 4.通配符匹配1.题目链接2.算法原理详解3.代码实现 1.最长公共子序列 1.题目链接 最长公共子序列 2.算法原理详…

Linux网络编程:数据链路层协议

目录 前言&#xff1a; 1.以太网 1.1.以太网帧格式 1.2.MTU&#xff08;最大传输单元&#xff09; 1.2.1.IP协议和MTU 1.2.2.UDP协议和MTU 1.2.3.TCP协议和MTU 2.ARP协议&#xff08;地址解析协议&#xff09; 2.1.ARP在局域网通信的角色 2.2.ARP报文格式 2.3.ARP报文…

SpringBoot高手之路02-全局异常处理器

RestControllerAdvice 可以将响应数据返回json格式然后响应 那么开始做全局异常处理器 首先先定义一个类 package com.healer.exception;import com.healer.common.Result; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.we…

AIGC专栏11——EasyAnimateV2结构详解与Lora训练 最大支持768x768 144帧视频生成

AIGC专栏11——EasyAnimateV2结构详解与Lora训练 最大支持768x768 144帧视频生成 学习前言源码下载地址EasyAnimate V2简介技术储备Diffusion Transformer (DiT)Motion ModuleU-VITLora 算法细节算法组成视频VAE视频DIT 数据处理视频分割视频筛选视频描述 模型训练视频VAE视频D…

Leetcode3168. 候诊室中的最少椅子数

Every day a Leetcode 题目来源&#xff1a;3168. 候诊室中的最少椅子数 解法1&#xff1a;模拟 代码&#xff1a; /** lc appleetcode.cn id3168 langcpp** [3168] 候诊室中的最少椅子数*/// lc codestart class Solution { public:int minimumChairs(string s){int chair…