Springboot+mybatis-plus+dynamic-datasource+继承DynamicRoutingDataSource切换数据源

Springboot+mybatis-plus+dynamic-datasource+继承DynamicRoutingDataSource切换数据源

背景

最近公司要求支持saas,实现动态切换库的操作,默认会加载主租户的数据源,其他租户数据源在使用过程中自动创建加入。

解决问题

1.通过请求中设置租户id 查询对应的库
2.通过设置上下文租户id 查询对应的库
3.测试mybatisplus mapper,service继承后设置上下文能否正常 查询对应的库

解决要求

1.改造现有系统尽量少改动,避免过多的耦合代码
2.已有功能正常
3.不影响之前的@DS注解切换数据源的

实现流程

1.代码结构

请添加图片描述

2.引入依赖

 <dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.14.3</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>4.2.0</version></dependency><dependency><groupId>org.testng</groupId><artifactId>testng</artifactId><version>7.4.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>

3.代码

3.1.TenantContextHolder

用于将租户id设置为上下文,获取当前的租户id

package com.liuhm.context;import com.alibaba.ttl.TransmittableThreadLocal;/*** saas 上下文 Holder*/
public class TenantContextHolder {/*** 当前租户编号*/private static final ThreadLocal<String> TENANT_ID = new TransmittableThreadLocal<>();/*** 获得租户编号。** @return 租户编号*/public static String getTenantId() {return TENANT_ID.get();}/*** 获得租户编号。如果不存在,则抛出 NullPointerException 异常** @return 租户编号*/public static String getRequiredTenantId() {String tenantId = getTenantId();if (tenantId == null) {throw new NullPointerException("TenantContextHolder 不存在租户编号!");}return tenantId;}public static void setTenantId(String tenantId) {TENANT_ID.set(tenantId);}public static void clear() {TENANT_ID.remove();}}
3.2.TenantWebFilter

拦截所有的请求获取header或者url中租户id的值,然后设置到上下文中。

(获取租户id可以改成获取token,并将租户id存入token值中,方便获取租户id)

package com.liuhm.config;import com.liuhm.context.TenantContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;public class TenantWebFilter extends OncePerRequestFilter {public static final String HEADER_TENANT_ID = "X-Tenant-Id";public static String getTenantId(HttpServletRequest request){String tenantId = StringUtils.hasLength(request.getHeader(HEADER_TENANT_ID)) ?request.getHeader(HEADER_TENANT_ID) :request.getHeader(HEADER_TENANT_ID.toLowerCase());if (StringUtils.isEmpty(tenantId)) {tenantId = getQueryParam(request.getQueryString(),HEADER_TENANT_ID);}return StringUtils.hasText(tenantId) ? tenantId : null;}public static String getQueryParam(String query,String key){if(Objects.isNull(query)){return null;}String[] params = query.split("&");for (String param : params) {String[] keyValue = param.split("=");if(Objects.equals(key.toLowerCase(),keyValue[0].toLowerCase()) && keyValue.length > 1){return keyValue[1];}}return null;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException{if (request.getRequestURI().equalsIgnoreCase("/harbor/clear")) {chain.doFilter(request, response);} else {String tenantId = getTenantId(request);if (tenantId != null) {TenantContextHolder.setTenantId(tenantId);}try {chain.doFilter(request, response);} finally {// 清理TenantContextHolder.clear();}}}
}
3.3 MyDynamicRoutingDataSource
  • MyDynamicRoutingDataSource继承DynamicRoutingDataSource 重新修改选择数据源的逻辑。

  • DynamicDataSourceContextHolder.peek()为空时,表示原功能默认的@DS没有设置,就通过tenantId去获取数据源

  • getDataSourceProperty 通过tenantId 获取数据源的配置信息

  • createDatasourceIfAbsent 通过配置信息去创建数据源并加入到dataSourceMap中

  • 通过对应的key去获取对应的数据源

package com.liuhm.config;import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.liuhm.context.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;
import java.util.Set;/*** @ClassName:MyDynamicRoutingDataSource* @Description: TODO* @Author: liuhaomin* @Date: 2024/5/9 8:44*/
@Slf4j
public class MyDynamicRoutingDataSource extends DynamicRoutingDataSource {@Overridepublic DataSource determineDataSource() {if(DynamicDataSourceContextHolder.peek() == null){String tenantId = TenantContextHolder.getTenantId();if(tenantId == null){throw new RuntimeException("租户id不能为空");}DataSourceProperty dataSourceProperty = getDataSourceProperty(tenantId);createDatasourceIfAbsent(dataSourceProperty);return getDataSource(tenantId);}else {DataSourceProperty dataSourceProperty = getDataSourceProperty(DynamicDataSourceContextHolder.peek());createDatasourceIfAbsent(dataSourceProperty);return super.determineDataSource();}}public MyDynamicRoutingDataSource(List<DynamicDataSourceProvider> providers) {super(providers);}/*** 用于创建租户数据源的 Creator*/@Resource@Lazyprivate DefaultDataSourceCreator dataSourceCreator;@Resource@Lazyprivate DynamicDataSourceProperties dynamicDataSourceProperties;@Value("${spring.datasource.dynamic.primaryDatabase}")private String primaryDatabase;public DataSourceProperty getDataSourceProperty(String tenantId){DataSourceProperty dataSourceProperty = new DataSourceProperty();DataSourceProperty primaryDataSourceProperty = dynamicDataSourceProperties.getDatasource().get(dynamicDataSourceProperties.getPrimary());BeanUtils.copyProperties(primaryDataSourceProperty,dataSourceProperty);dataSourceProperty.setUrl(dataSourceProperty.getUrl().replace(primaryDatabase,tenantId));dataSourceProperty.setPoolName(tenantId);return dataSourceProperty;}private String createDatasourceIfAbsent(DataSourceProperty dataSourceProperty){// 1. 重点:如果数据源不存在,则进行创建if (isDataSourceNotExist(dataSourceProperty)) {// 问题一:为什么要加锁?因为,如果多个线程同时执行到这里,会导致多次创建数据源// 问题二:为什么要使用 poolName 加锁?保证多个不同的 poolName 可以并发创建数据源// 问题三:为什么要使用 intern 方法?因为,intern 方法,会返回一个字符串的常量池中的引用// intern 的说明,可见 https://www.cnblogs.com/xrq730/p/6662232.html 文章synchronized(dataSourceProperty.getPoolName().intern()){if (isDataSourceNotExist(dataSourceProperty)) {log.debug("创建数据源:{}", dataSourceProperty.getPoolName());DataSource dataSource = null;try {dataSource = dataSourceCreator.createDataSource(dataSourceProperty);}catch (Exception e){log.error("e {}",e);if(e.getMessage().contains("Unknown database")){throw new RuntimeException("租户不存在");}throw e;}addDataSource(dataSourceProperty.getPoolName(), dataSource);}}} else {log.debug("数据源已存在,无需创建:{}", dataSourceProperty.getPoolName());}// 2. 返回数据源的名字return dataSourceProperty.getPoolName();}private boolean isDataSourceNotExist(DataSourceProperty dataSourceProperty){return !getDataSources().containsKey(dataSourceProperty.getPoolName());}
}
3.4.TenantAutoConfiguration
  • TenantWebFilter加入FilterRegistrationBean
  • 创建 MyDynamicRoutingDataSource Bean
package com.liuhm.config;import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;
import java.util.List;@Configuration
public class TenantAutoConfiguration {@Beanpublic FilterRegistrationBean<TenantWebFilter> tenantContextWebFilter() {FilterRegistrationBean<TenantWebFilter> registrationBean = new FilterRegistrationBean<>();registrationBean.setFilter(new TenantWebFilter());registrationBean.setOrder(-104);return registrationBean;}@Autowiredprivate DynamicDataSourceProperties properties;@Beanpublic DataSource dataSource(List<DynamicDataSourceProvider> providers) {MyDynamicRoutingDataSource dataSource = new MyDynamicRoutingDataSource(providers);dataSource.setPrimary(properties.getPrimary());dataSource.setStrict(properties.getStrict());dataSource.setStrategy(properties.getStrategy());dataSource.setP6spy(properties.getP6spy());dataSource.setSeata(properties.getSeata());dataSource.setGraceDestroy(properties.getGraceDestroy());return dataSource;}
}

4.总结

4.1.多租户切换的方法
  1. dynamic-datasource 跨库进行切换数据源可以用DynamicDataSourceContextHolder.push()
  • 在过滤器[filter]里切换
  • 拦截器里切换数据源
  • 方法内部硬编码切换
  • 通过service,mapper加注解进行切换@DS (不推荐,有切面没有切成功的,如本类调用自己的方法)
  1. 重写DynamicRoutingDataSource选择器,自定义上下文获取租户id获取对应的DataSource
4.2.上诉方法中都可以实现
  • 过滤器和拦截器切换数据源的时候,线程执行的方法不容切换,需要手动切换,或者在设置租户id的时候进行切换数据源。(耦合性过大,代码不够单一,如果在设置租户id的时候去切换数据源)
  • 重写DynamicRoutingDataSource选择器,只是在执行sql前进行数据源获取的切换,耦合性小,代码单一性好,且不影响之前的功能。
4.3.设置租户id需要注意的
  • 所有请求需要拦截进行设置
  • 所有线程需要相关的需要进行重写并设置租户上下文
  • 所有fegin需要进行设置租户上下文
  • 以上4.3的操作可以学习一下mdc链路追踪日志的代码

编码不易,有问题多多指教

博客地址

代码下载

下面的springcloud_dynamic_datasource_tenant

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用户页面触发点击事件和 js 执行点击事件的区别

文章目录 情景展示情况一&#xff1a;用户点击页面触发情况二&#xff1a;通过 js 触发点击 结果分析情况一情况二 其实这个谜底揭开之后&#xff0c;第一反应都是&#xff0c;哦~&#xff0c;非常简单&#xff0c;但是细节决定成败&#xff0c;我被这个细节毁掉了&#xff0c;…

【十大排序算法】----选择排序(详细图解分析+实现,小白一看就会)

目录 一&#xff1a;选择排序——原理 二&#xff1a;选择排序——分析 三&#xff1a;选择排序——实现 四&#xff1a;选择排序——优化 五&#xff1a;选择排序——效率 一&#xff1a;选择排序——原理 选择排序的原理&#xff1a;通过遍历数组&#xff0c;选出该数组…

精酿啤酒:精酿文化的传承者与创新者

在啤酒的世界中&#xff0c;精酿啤酒是一种与众不同的文化现象。这种文化源于对啤酒品质的追求和对传统工艺的尊重&#xff0c;但在不断发展中也不断涌现出创新。作为精酿啤酒的品牌&#xff0c;Fendi club啤酒不仅是这种文化的传承者&#xff0c;更是创新者。 Fendi club啤酒始…

香港电讯高效网络,助力新消费品牌抓住拓展香港市场新风口

自今年初香港与内地全面恢复通关&#xff0c;两地同胞跨境消费热潮持续升温。港人“北上”消费掀起风潮的同时&#xff0c;香港市场也成为内地新消费品牌拓展的热门目标。从糕点、茶饮、连锁餐饮到服饰&#xff0c;越来越多内地品牌进驻香港。新消费品牌要想在香港开设门店&…

QT部分学习笔记

文章目录 1.前言注意问题2.学习历程2.0 创建项目 快捷键&#xff1a;2.1 创建按钮2.2 对象树2.3 调试输出2.4 QT坐标系2.5 信号和槽 3.Qmainwindow3.1 窗口菜单栏创建3.2 工具栏3.3 状态栏3.4 铆接部件3.5 对话框 4. 1.前言 版本&#xff1a; 5.9.9 注意问题 Qstring类型通多…

算法课程笔记——蓝桥云课第11次直播

算法课程笔记——蓝桥云课第11次直播

ORA-28575: unable to open RPC connection to external procedure agent

环境&#xff1a; Oracle 11.2.0.4x64 RAC AIX6.1版本SDE for aix oracle11g版本10.0 x64 sde配置情况如下&#xff1a; 检查oracle和grid用户下的$ORACLE_HOME/hs/admin/extproc.ora文件均包含有如下&#xff1a; SET EXTPROC_DLLSANY 两个节点sde下的user_libraries都正常…

leetcode.环形链表问题

目录 题目1 示例 解题思路 代码实现 补充 题目2 示例 解题思路 代码实现 题目1 该题链接&#xff1a;https://leetcode.cn/problems/linked-list-cycle/description/ 示例 解题思路 要创建两个指针一个是快指针(fast)&#xff0c;另一个慢指针(slow)。快指针走两步慢指…

【Android】Apk图标的提取、相同目录下相同包名提取的不同图标apk但是提取结果相同的bug解决

一般安卓提取apk图标我们有两种常用方法&#xff1a; 1、如果已经获取到 ApplicationInfo 对象&#xff08;假设名为 appInfo&#xff09;&#xff0c;那么我们获取方法为&#xff1a; appInfo.loadIcon(packageManager)// 返回一个 Drawable 对象2、 如果还没获取到 Applica…

必背!!2024年软考中级——网络工程师考前冲刺几页纸

距离软考考试的时间越来越近了&#xff0c;趁着这两周赶紧准备起来 今天给大家整理了——软考网络工程师考前冲刺几页纸&#xff0c;都是核心重点&#xff0c;有PDF版&#xff0c;可打印下来&#xff0c;每天背一点。 计算机总线分类 ①总线的分类&#xff1a;数据总线、地址总…

内网渗透瑞士军刀-impacket工具解析(二)

impacket工具解析之Kerberos认证协议 上一期我们介绍了impacket中ntlm协议的实现&#xff0c;在Windows认证中除了使用ntlm认证&#xff0c;还支持Kerberos认证协议&#xff0c;Kerberos认证也是Windows 活动目录中占比最高的认证方式。 什么是Kerberos协议&#xff1f; Kerb…

CSRF 攻击实验:更改请求方式绕过验证

前言 CSRF&#xff08;Cross-Site Request Forgery&#xff09;&#xff0c;也称为XSRF&#xff0c;是一种安全漏洞&#xff0c;攻击者通过欺骗用户在受信任网站上执行非自愿的操作&#xff0c;以实现未经授权的请求。 CSRF攻击利用了网站对用户提交的请求缺乏充分验证和防范…

英语学习笔记10——Look at ...

Look at … 看…… 词汇 Vocabulary fat adj. 胖的&#xff0c;丰富的 n. 脂肪 例句&#xff1a;他是个胖男孩。    He is a fat boy. 搭配&#xff1a;fat cat 有钱人&#xff0c;土豪 woman n. 女人 girl n. 女孩 madam n. 女士 man n. 男人 boy n. 男孩 sir n. 先生 …

jdk安装多个版本,但是java -version显示最早安装的版本,换掉Path或者JAVA_HOME不生效问题

问题一&#xff1a;当你的电脑上又多个jdk版本&#xff0c;如17 或者8时&#xff0c;使用命令行 java -version显示最早安装的&#xff0c;如下图所示&#xff1a;环境变量配置的17&#xff0c;但是命令行显示的是8。 原因&#xff1a;windows电脑装jdk17后 会在你的环境变量…

声纹识别在无人机探测上的应用

无人机在民用和军事领域的应用越来越广泛。然而&#xff0c;随着无人机数量的增加&#xff0c;"黑飞"现象也日益严重&#xff0c;对公共安全和隐私构成了威胁。因此&#xff0c;开发有效的无人机探测与识别技术变得尤为重要。及时发现黑飞无人机的存在进而对其型号进…

Springboot+Vue项目-基于Java+MySQL的民族婚纱预定系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

在浏览器执行js脚本的两种方式

fetch请求get 在浏览器执行http请求,可以使用fetch函数; fetch(“url”).then(response => response.text()) .then(data => console.log(JSON.parse(data)[‘status’])) .catch(error => console.error(error)) 直接返回json数据: fetch(“url”).then(response…

【Java】/*数组的定义与使用*/

目录 一、数组的定义 1.1 为什么要使用数组 1.2 什么是数组 1.3 数组的初始化 1.3.1 动态初始化 1.3.2 静态初始化 1.2.3 注意事项 三、遍历数组 3.1 用循环的方式遍历数组 3.2 用 for each 的方式遍历数组 3.3 用 Arrays.toString 的方式遍历数组 3.4 一些其他的…

【3dmax笔记】028:倒角的使用方法

一、倒角描述 在3dmax中创建倒角效果可以通过多种方法实现,以下是几种常见的方法: 使用倒角修改器。首先创建一个图形(如矩形和圆),然后对齐它们,将它们转化为可编辑样条线,并附加在一起,选择要倒角的边缘,然后使用倒角修改器来调整高度、轮廓等参数。使用倒角剖面修…

【稳定检索|投稿优惠】2024年医学、药学与生物工程国际会议(ICMPB 2024)

2024年医学、药学与生物工程国际会议&#xff08;ICMPB 2024&#xff09; 2024 International Conference on Medicine, Pharmacy, and Biotechnology 【会议简介】 2024年医学、药学与生物工程国际会议将于长沙召开。此次会议将汇聚全球医学、药学与生物工程领域的顶尖学者…