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;各个…

web入门练手案例(二)

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

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; …

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

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

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;可能需要重复下数据库设计的范式原则&#…

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

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…

Debian12安装后更换为国内镜像源,切换root用户,解决用户名不在sudoers文件中此事将被报告

选择Debian作为编程开发最佳Linux的理由&#xff1a; Debian是面向程序员的最古老&#xff0c;最出色的Linux发行版之一。Debian提供了具有.deb软件包管理兼容性的超稳定发行版。Debian为程序员提供了许多最新功能。因此&#xff0c;它具有一个特殊的编程空间。Debian是开发人员…

弥合孤岛:克服构建 DevOps 文化的挑战

持续变革正在发生软件开发行业。DevOps 因其对自动化、协作和持续改进的关注而成为优化软件交付并弥合开发和运营团队之间鸿沟的重要方法。然而&#xff0c;过渡到真正的 DevOps 文化并非没有挑战。本文探讨了您在追求 DevOps 时可能面临的障碍并提供了解决方案。 01 了解 Dev…

数据结构 顺序表1

1. 何为顺序表&#xff1a; 顺序表是一种线性数据结构&#xff0c;是由一组地址连续的存储单元依次存储数据元素的结构&#xff0c;通常采用数组来实现。顺序表的特点是可以随机存取其中的任何一个元素&#xff0c;并且支持在任意位置上进行插入和删除操作。在顺序表中&#xf…

算法-卡尔曼滤波之基本数学的概念

1.均值 定义&#xff1a;均值是一组数据中所有数值的总和除以数据的数量。均值是数据的中心趋势的一种度量&#xff0c;通常用符号 xˉ 表示。 &#xff1a;对于包含 n 个数据的数据集 {&#x1d465;1,&#x1d465;2,...,&#x1d465;&#x1d45b;}&#xff0c;均值 xˉ 计…

Spring原理

目录 一、Bean作用域 1、Bean作用域概念 2、Bean的六种作用域 3、举例演示bean的几种作用域 二、Bean的生命周期 1、Bean对象的生命周期 2、演示Bean对象的生命周期 三、SpringBoot的自动配置 1、概念 2、spring将对象存入容器中的原理 一、Bean作用域 在前面spring…

C语言(指针)7

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

6款日常精选手机APP推荐!

AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/ 1.全能相机软件——无他相机 无他相机App是一款完全免费且功能全面的美颜相机软件。这款相机应用集自拍、美颜、图片编辑、风格化模板、流行贴…