网站首页 > 教程文章 正文
基本概念
基本的多租户逻辑意思就是,业务逻辑一样,但是需要根据不同业务方动态切换不同的数据库。这样做的好处就是业务方数据物理隔离,天然分库,数据量和业务控制更加灵活。
基本交互流程
下图是我们最近实现的多租户架构,其实很简单:
1,用户登录库是同一个,登录成功返回用户所在业务方,失败返回失败信息给用户
2,网关拿到用户的业务标识后,透传给下游业务系统,业务系统链接多个datasource
3,业务系统,在filter里根据用户标识,选择datasource数据源
4,最后在操作数据库时由于数据源已经被切换,会路由到正确的数据库
技术选型
基本的架构是springboot+MySQL,动态数据源切换需要引入dynamic-datasource,进行数据源切换管理等工作
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
业务代码开发
第一步需要在filter里进行拦截,并切换数据源,一定是filter,不要在拦截器里写,因为filter的执行优先级大于拦截器,另外如果有多个filter的情况下,一定要先执行数据源切换的filter。
filter执行最后,记得一定要
DynamicDataSourceContextHolder.clear()下,如果有兴趣的同学可以点开看下源码,内部其实是通过ThreadLocal传递库信息
具体代码如下:
package com.ruoyi.common.core.filter;
@Order(-10000)
@Slf4j
@Component
public class DynamicSwitchDbFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
//在DispatcherServlet之前执行
HttpServletRequest req = (HttpServletRequest) request;
//业务方决定对应的库
String orgCode = req.getHeader("org_code");
log.info(req.getRequestURI());
if (StringUtils.isBlank(orgCode)) {
log.error("header org code is null,can't find db");
throw new BaseException(400, "header org code is null,can't find db");
}
log.info("switch db name:{}", orgCode);
//切换数据源
DynamicDataSourceContextHolder.push(orgCode);
filterChain.doFilter(request, response);
//通过ThreadLocal传递库信息,所以需要清楚ThreadLocal
log.info("clean db name:{}", orgCode);
DynamicDataSourceContextHolder.clear();
}
@Override
public void destroy() {
}
}
第二步:xml配置多数据源:下面这个配置是完整的配置,xiaomi和huawei是数据源名称,代码里通过
DynamicDataSourceContextHolder.push(“huawei”);切换数据源
spring:
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
dynamic:
druid:
initial-size: 3
min-idle: 3
maxActive: 10
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,slf4j
connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
datasource:
# 主库数据源
xiaomi:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
username: xxx
password: xxx
huawei:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxx.xx.xxx.xxx:3306/test2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
username: xxx
password: xxx
以上两步就可以实现数据源切换了,但实际场景比这个复杂的多,DynamicDataSource默认的数据源是master,如果找不到其它的数据源,就会使用master数据源,我们不建议使用这个master数据源。
第三步:去掉DynamicRoutingDataSource的默认数据源master,如下代码可以直接粘贴使用
package com.ruoyi.common.core.service;
/**
* 重写DynamicRoutingDataSource,让dynamicRoutingDataSource主加载,重写目的是在找不到库名时直接报异常,不去加载主库,多租户场景没有主库的概念
*/
@Primary
@Component("dynamicRoutingDataSource")
public class RewriteDynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
private static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
private static final String UNDERLINE = "_";
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap();
private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap();
@Autowired
private List<DynamicDataSourceProvider> providers;
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
//多租户没有主库概念,没有找到租户对应的数据库,直接报错.
//改动点一
//private String primary = "master";
private String primary = "xxxx";
private Boolean strict = false;
private Boolean p6spy = false;
private Boolean seata = false;
public RewriteDynamicRoutingDataSource(List<DynamicDataSourceProvider> providers) {
this.providers = providers;
}
protected String getPrimary() {
return this.primary;
}
public DataSource determineDataSource() {
String dsKey = DynamicDataSourceContextHolder.peek();
return this.getDataSource(dsKey);
}
private DataSource determinePrimaryDataSource() {
log.debug("dynamic-datasource switch to the primary datasource");
DataSource dataSource = (DataSource)this.dataSourceMap.get(this.primary);
if (dataSource != null) {
return dataSource;
} else {
GroupDataSource groupDataSource = (GroupDataSource)this.groupDataSources.get(this.primary);
if (groupDataSource != null) {
return groupDataSource.determineDataSource();
} else {
throw new CannotFindDataSourceException("dynamic-datasource can not find primary datasource");
}
}
}
public Map<String, DataSource> getDataSources() {
return this.dataSourceMap;
}
public Map<String, GroupDataSource> getGroupDataSources() {
return this.groupDataSources;
}
//找不到不到数据库直接报错, 改动点二
public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
//找不到不到数据库直接报错
throw new CannotFindDataSourceException("dynamic-datasource can not find datasource:"+ds);
} else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) {
log.info("dynamic-datasource switch to the datasource named [{}]", ds);
return ((GroupDataSource)this.groupDataSources.get(ds)).determineDataSource();
} else if (this.dataSourceMap.containsKey(ds)) {
log.info("dynamic-datasource switch to the datasource named [{}]", ds);
return (DataSource)this.dataSourceMap.get(ds);
} else if (this.strict) {
throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
} else {
//找不到不到数据库直接报错
throw new CannotFindDataSourceException("dynamic-datasource can not find datasource:"+ds);
}
}
public synchronized void addDataSource(String ds, DataSource dataSource) {
DataSource oldDataSource = (DataSource)this.dataSourceMap.put(ds, dataSource);
this.addGroupDataSource(ds, dataSource);
if (oldDataSource != null) {
this.closeDataSource(ds, oldDataSource);
}
log.info("dynamic-datasource - add a datasource named [{}] success", ds);
}
private void addGroupDataSource(String ds, DataSource dataSource) {
if (ds.contains("_")) {
String group = ds.split("_")[0];
GroupDataSource groupDataSource = (GroupDataSource)this.groupDataSources.get(group);
if (groupDataSource == null) {
try {
groupDataSource = new GroupDataSource(group, (DynamicDataSourceStrategy)this.strategy.getDeclaredConstructor().newInstance());
this.groupDataSources.put(group, groupDataSource);
} catch (Exception var6) {
Exception e = var6;
throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
}
}
groupDataSource.addDatasource(ds, dataSource);
}
}
public synchronized void removeDataSource(String ds) {
if (!StringUtils.hasText(ds)) {
throw new RuntimeException("remove parameter could not be empty");
} else if (this.primary.equals(ds)) {
throw new RuntimeException("could not remove primary datasource");
} else {
if (this.dataSourceMap.containsKey(ds)) {
DataSource dataSource = (DataSource)this.dataSourceMap.remove(ds);
this.closeDataSource(ds, dataSource);
if (ds.contains("_")) {
String group = ds.split("_")[0];
if (this.groupDataSources.containsKey(group)) {
DataSource oldDataSource = ((GroupDataSource)this.groupDataSources.get(group)).removeDatasource(ds);
if (oldDataSource == null) {
log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
}
}
}
log.info("dynamic-datasource - remove the database named [{}] success", ds);
} else {
log.warn("dynamic-datasource - could not find a database named [{}]", ds);
}
}
}
public void destroy() throws Exception {
log.info("dynamic-datasource start closing ....");
Iterator var1 = this.dataSourceMap.entrySet().iterator();
while(var1.hasNext()) {
Map.Entry<String, DataSource> item = (Map.Entry)var1.next();
this.closeDataSource((String)item.getKey(), (DataSource)item.getValue());
}
log.info("dynamic-datasource all closed success,bye");
}
public void afterPropertiesSet() throws Exception {
this.checkEnv();
Map<String, DataSource> dataSources = new HashMap(16);
Iterator var2 = this.providers.iterator();
while(var2.hasNext()) {
DynamicDataSourceProvider provider = (DynamicDataSourceProvider)var2.next();
dataSources.putAll(provider.loadDataSources());
}
var2 = dataSources.entrySet().iterator();
while(var2.hasNext()) {
Map.Entry<String, DataSource> dsItem = (Map.Entry)var2.next();
this.addDataSource((String)dsItem.getKey(), (DataSource)dsItem.getValue());
}
if (this.groupDataSources.containsKey(this.primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), this.primary);
} else if (this.dataSourceMap.containsKey(this.primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), this.primary);
} else {
log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
}
}
private void checkEnv() {
Exception e;
if (this.p6spy) {
try {
Class.forName("com.p6spy.engine.spy.P6DataSource");
log.info("dynamic-datasource detect P6SPY plugin and enabled it");
} catch (Exception var3) {
e = var3;
throw new RuntimeException("dynamic-datasource enabled P6SPY ,however without p6spy dependency", e);
}
}
if (this.seata) {
try {
Class.forName("io.seata.rm.datasource.DataSourceProxy");
log.info("dynamic-datasource detect ALIBABA SEATA and enabled it");
} catch (Exception var2) {
e = var2;
throw new RuntimeException("dynamic-datasource enabled ALIBABA SEATA,however without seata dependency", e);
}
}
}
private void closeDataSource(String ds, DataSource dataSource) {
try {
if (dataSource instanceof ItemDataSource) {
((ItemDataSource)dataSource).close();
} else {
if (this.seata && dataSource instanceof DataSourceProxy) {
DataSourceProxy dataSourceProxy = (DataSourceProxy)dataSource;
dataSource = dataSourceProxy.getTargetDataSource();
}
if (this.p6spy && dataSource instanceof P6DataSource) {
Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
realDataSourceField.setAccessible(true);
dataSource = (DataSource)realDataSourceField.get(dataSource);
}
Method closeMethod = ReflectionUtils.findMethod(dataSource.getClass(), "close");
if (closeMethod != null) {
closeMethod.invoke(dataSource);
}
}
} catch (Exception var4) {
Exception e = var4;
log.warn("dynamic-datasource closed datasource named [{}] failed", ds, e);
}
}
public void setStrategy(Class<? extends DynamicDataSourceStrategy> strategy) {
this.strategy = strategy;
}
public void setPrimary(String primary) {
this.primary = primary;
}
public void setStrict(Boolean strict) {
this.strict = strict;
}
public void setP6spy(Boolean p6spy) {
this.p6spy = p6spy;
}
public void setSeata(Boolean seata) {
this.seata = seata;
}
}
其它问题
在动态数据源DynamicRoutingDataSource实际工作中会涉及到内部调用的情况,如redis,任务调度,fegin调用,httpclient调用以及项目启动预加载数据等场景,这些场景无法确定使用哪个库,还有更复杂的多线程无法传递datasource的情况,我会持续更新相应解决方案。
猜你喜欢
- 2025-09-13 SDN、NFV、云网融合傻傻分不清?理清它们的来龙去脉与应用逻辑
- 2025-09-13 Java多租户SaaS系统实现方案_多租户 springboot
- 2025-09-13 【推荐】一款开源免费、功能强大的多租户 Saas 快速开发平台
- 2025-09-13 EFCore多租户实现-共享数据库模式
- 2025-09-13 这款 .NET 9 + React 后台权限管理系统太强了!支持多租户、按钮权限(简单易用且文档齐全)
- 2025-09-13 设计一个多租户 SaaS 系统,如何实现租户数据隔离(...
- 2025-09-13 “凭什么要比业主多收钱?”租户抱怨遭区别对待,小区管理方喊委屈……
- 2025-09-13 设计一个多租户 SaaS 系统,如何实现租户数据隔离与资源配额控制?
- 2025-09-13 多租户登录页设计全攻略:如何实现灵活切换与数据隔离?
- 2025-09-13 一文掌握 Spring Boot 数据库热插拔与多租户支持
- 最近发表
-
- K8s 部署频繁出错?一套流程教你快速定位故障,工作效率翻倍
- 防火墙服务无法启用,显示灰色的解决办法
- 网络问题-电脑无法上网处理思路以及办法 (总集)
- Win10学院:Windows Denfender无法启动怎么办?
- Windows账户登录问题解决方案_登录windows账户什么意思
- IIS无法启动提示计算机上"."的服务W3SVC,一分钟搞定,抓紧收藏
- 已申请到免费顶级域名如何管理_顶级域名免费注册
- 网站被劫持了老是跳转怎么办_网站被劫持到其它网站如何解决
- 这些“常用药”被注销!涉及维生素、去痛片、眼药水等!快看看你家有吗?
- 《皕宋楼藏书志》清 藏书家陆心源与其门人李宗莲合编的藏书目录
- 标签列表
-
- location.href (44)
- document.ready (36)
- git checkout -b (34)
- 跃点数 (35)
- 阿里云镜像地址 (33)
- qt qmessagebox (36)
- mybatis plus page (35)
- vue @scroll (38)
- 堆栈区别 (33)
- 什么是容器 (33)
- sha1 md5 (33)
- navicat导出数据 (34)
- 阿里云acp考试 (33)
- 阿里云 nacos (34)
- redhat官网下载镜像 (36)
- srs服务器 (33)
- pico开发者 (33)
- https的端口号 (34)
- vscode更改主题 (35)
- 阿里云资源池 (34)
- os.path.join (33)
- redis aof rdb 区别 (33)
- 302跳转 (33)
- http method (35)
- js array splice (33)