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.mzph.cn/diannao/12196.shtml

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

相关文章

数据可视化训练第7天(json文件读取国家人口数据,找出前10和后10)

数据 https://restcountries.com/v3.1/all&#xff1b;建议下载下来&#xff0c;并不是很大 import numpy as np import matplotlib.pyplot as plt import requests import json #由于访问url过于慢&#xff1b;将数据下载到本地是json数据 #urlhttps://restcountries.com/v3…

MATLAB蚁群算法求解带时间窗的旅行商TSPTW问题代码实例

MATLAB蚁群算法求解带时间窗的旅行商TSPTW问题代码实例 蚁群算法编程求解TSPTW问题实例&#xff1a; 在经纬度范围为(121, 43)到(123, 45)的矩形区域内&#xff0c;散布着1个商家&#xff08;编号1&#xff09;和25个顾客点&#xff08;编号为226&#xff09;&#xff0c;各个…

前端工程化实践:Monorepo与Lerna管理

前端工程化实践中&#xff0c;Monorepo&#xff08;单仓库&#xff09;管理和Lerna是两种流行的方式&#xff0c;用于大型项目或组件库的组织和版本管理。 2500G计算机入门到高级架构师开发资料超级大礼包免费送&#xff01; Monorepo简介 Monorepo&#xff08;单仓库&#…

web入门练手案例(二)

下面是一下web入门案例和实现的代码&#xff0c;带有部分注释&#xff0c;倘若代码中有任何问题或疑问&#xff0c;欢迎留言交流~ 数字变色Logo 案例描述 “Logo”是“商标”的英文说法&#xff0c;是企业最基本的视觉识别形象&#xff0c;通过商标的推广可以让消费者了解企…

第一个Rust程序

在安装好Rust以后&#xff0c;我们就可以编写程序了。 首先&#xff0c;我们执行下面的命令&#xff0c;尽量让你的rust版本和我的版本相同&#xff0c;或者比我的版本大。 zhangdapengzhangdapeng:~$ cargo --version cargo 1.78.0 (54d8815d0 2024-03-26) zhangdapengzhangd…

C语言(指针)2

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…

听说SOLIDWORKS科研版可以节约研发成本?

近几年来&#xff0c;政府越来越重视科研带动产业&#xff0c;绩效优良的产业技术研究院对于国家和地区的学术成果转化、技术创新、产业发展等具有不可忽视的促进和带动作用。研究院会承担众多新产业的基础研究工作&#xff0c;而常规的基础研究需要长期的积累&#xff0c;每个…

JAVA毕业设计141—基于Java+Springboot+Vue的物业管理系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootVue的物业管理系统(源代码数据库)141 一、系统介绍 本项目前后端分离&#xff0c;分为管理员、员工、用户三种角色(角色权限可自行分配) 1、用户&#xff1a; …

Nginx详解:高性能HTTP和反向代理服务器

Nginx详解&#xff1a;高性能HTTP和反向代理服务器 一、引言 Nginx&#xff08;发音为“engine x”&#xff09;是一个开源的高性能HTTP和反向代理服务器&#xff0c;也是一个IMAP/POP3/SMTP代理服务器。由于其出色的性能和稳定性&#xff0c;Nginx已经成为互联网上最受欢迎的…

asp.net结课作业中遇到的问题解决4

目录 1、vs2019每次运行一次项目之后&#xff0c;样式表的格式就算在vs2019上改变了&#xff0c;在浏览器中显示的还是以前的样式&#xff0c;所以应该如何修改 2、如何实现选择下拉框之后&#xff0c;显示所选择的这个类型的书籍的名称 3、如何实现点击首页显示的书籍&#…

高清模拟视频采集卡CVBS四合一信号采集设备解析

介绍一款新产品——LCC261高清视频采集与编解码一体化采集卡。这款高品质的产品拥有卓越的性能表现和丰富多样的功能特性&#xff0c;能够满足广大用户对于高清视频采集、处理以及传输的需求。 首先&#xff0c;让我们来了解一下LCC261的基本信息。它是一款基于灵卡技术研发的高…

Shell三剑客之sed

前言&#xff1a; Shell三剑客是grep、sed和awk三个工具的简称,因功能强大&#xff0c;使用方便且使用频率高&#xff0c;因此被戏称为三剑客&#xff0c;熟练使用这三个工具可以极大地提升运维效率。 sed是一个流编辑器&#xff0c;用于对文本进行编辑、替换、删除等操作。sed…

LeetCode2095删除链表的中间节点

题目描述 给你一个链表的头节点 head 。删除 链表的 中间节点 &#xff0c;并返回修改后的链表的头节点 head 。长度为 n 链表的中间节点是从头数起第 ⌊n / 2⌋ 个节点&#xff08;下标从 0 开始&#xff09;&#xff0c;其中 ⌊x⌋ 表示小于或等于 x 的最大整数。对于 n 1、…

深入探索Android签名机制:从v1到v3的演进之旅

引言 在Android开发的世界中&#xff0c;APK的签名机制是确保应用安全性的关键环节。随着技术的不断进步&#xff0c;Android签名机制也经历了从v1到v3的演进。本文将带你深入了解Android签名机制的演变过程&#xff0c;揭示每个版本背后的技术细节&#xff0c;并探讨它们对开…

浅谈下MYSQL表设计的几条规则

作为后端开发人员&#xff0c;避免不了和数据库打交道&#xff0c;可是我们怎么能够设计出高效&#xff0c;可维护&#xff0c;可扩展的数据库设计呢&#xff0c;在这里我总结了几个点&#xff0c;供大家参考。 在写之前&#xff0c;可能需要重复下数据库设计的范式原则&#…

docker-compose.yml文件详解

创建 docker-compose.yml 文件是使用 Docker Compose 管理多容器应用的第一步。这个 YAML 格式的文件详细描述了服务、网络和卷等组件以及它们之间的关系。下面是对一个典型 docker-compose.yml 文件结构的详解&#xff1a; 基本结构 一个基本的 docker-compose.yml 文件通常…

水雨情监测系统—实时监测水位信息

TH-SW3水雨情监测系统是一种专门用于实时监测和收集水文气象数据的自动化系统。它能够实时获取区域内降雨和水情数据&#xff0c;并将其存储到数据库中进行分析处理&#xff0c;从而为防汛指挥人员提供及时准确的信息服务。 水雨情监测系统的主要功能包括实时监测水位、流速、流…

C++类与对象基础探秘系列(二)

目录 类的6个默认成员函数 构造函数 构造函数的概念 构造函数的特性 析构函数 析构函数的概念 析构函数的特性 拷贝构造函数 拷贝构造函数的概念 拷贝构造函数的特性 赋值运算符重载 运算符重载 赋值运算符重载 const成员 const修饰类的成员函数 取地址及const取地址操作…

MySQL文档_下载

可能需要&#xff1a;MySQL下载–》更新版本–》迁移数据库到MySQL 以下都不重要【只要确定好需要安装版本&#xff0c;找到对应的版本下载&#xff0c;安装&#xff0c;设置即可】 下载、安装&#xff1a; Determine whether MySQL runs and is supported on your platform…

iCloud如何高效利用:提升苹果生态体验

iCloud如何高效利用&#xff1a;提升苹果生态体验 引言 iCloud是苹果公司提供的云服务&#xff0c;它允许用户在苹果设备之间无缝同步数据和内容。随着数字化生活的不断发展&#xff0c;有效地管理和利用iCloud对于提高工作效率和生活质量变得越来越重要。本文将详细介绍如何…