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.pswp.cn/diannao/12196.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/12196.shtml
英文地址,請注明出處:http://en.pswp.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對于提高工作效率和生活質量變得越來越重要。本文將詳細介紹如何…