SpringBoot集成Mybatis-Plus实现多租户动态数据源

news/2024/4/25 16:15:20/文章来源:https://blog.csdn.net/liu320yj/article/details/130125139

1. 概述

最近接手一个多租户系统,多租户主要的就是租户之间的数据是相互隔离的,每个租户拥有自己独立的数据,相互之间不干扰。目前实现多租户主要有三种方案:
独立数据库
每个租户拥有自己单独的数据库,从物理上隔离了自己的数据,安全性最高,但是成本比较高,容易浪费数据库资源
同一数据库,不同表
每个租户的数据都在同一个数据库里,每个租户拥有一个独立的表,同样也实现了数据的隔离,安全性和成本其次
同一数据库,同一张表,字段区分
租户使用同一个数据库和同一张表,在每张表里添加进一个字段,例如tenant来区分每个租户的数据,安全性和成本都比较低,维护性也较高,单表的数据量也比较大,给查询和数据迁移都来带了麻烦
基于以上方案,本文选择第一种方案实现多租户系统

2. 开发环境

本文使用使用的开发工具/组件如表所示:

名称版本
Idea2020
JDK11
SpringBoot2.7.10
mybatis-plus-boot-starter3.5.3.1
dynamic-datasource-spring-boot-starter3.6.1
druid-spring-boot-starter1.2.14
mapstruct1.5.3.Final
postgresql15.2
redis7.0.10

3. 搭建项目

3.1. 新建数据库和表

先建几个数据库,分别是dynamic-master、dynamic-slave-1和dynamic-slave-2,在master库中新建tenant表,在slave库中建customer表,建表sql如下:

CREATE SEQUENCE IF NOT EXISTS tenant_id_seq;
CREATE TABLE public.tenant (id bigint NOT null DEFAULT nextval('tenant_id_seq'),tenant_id varchar(30) NOT NULL,data_source_url varchar(100) NOT NULL,data_source_username varchar(30) NOT NULL,data_source_password varchar(68) NOT NULL,data_source_driver varchar(50) NOT NULL,data_source_poolname varchar(50) NOT NULL,CONSTRAINT tenant_pk PRIMARY KEY (id),CONSTRAINT tenant_un UNIQUE (tenant_id)
);
COMMENT ON TABLE "tenant" IS '租户表';
COMMENT ON COLUMN "tenant"."tenant_id" IS '租户id';
COMMENT ON COLUMN "tenant"."data_source_url" IS '数据源URL';
COMMENT ON COLUMN "tenant"."data_source_username" IS '数据源用户名';
COMMENT ON COLUMN "tenant"."data_source_password" IS '数据源密码';
COMMENT ON COLUMN "tenant"."data_source_driver" IS '数据源驱动';
COMMENT ON COLUMN "tenant"."data_source_poolname" IS '数据源池名称';CREATE SEQUENCE IF NOT exists customer_id_seq;
CREATE TABLE public.customer (id bigint NOT NULL DEFAULT nextval('customer_id_seq'),customer_name varchar(30) NOT NULL,CONSTRAINT customer_pk PRIMARY KEY (id)
);
COMMENT ON TABLE public.customer IS '客户表';
COMMENT ON COLUMN public.customer.id IS '客户ID';
COMMENT ON COLUMN public.customer.customer_name IS '客户名称';

3.2. 引入核心依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions>
</dependency>
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency>
<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.6.1</version><exclusions><exclusion><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></exclusion></exclusions>
</dependency>
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.14</version>
</dependency>
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope>
</dependency>
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>1.5.3.Final</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

3.3. 编写application.yml文件

server:port: 8000
spring:application:name: SPRINGBOOT-TENANTdatasource:dynamic:primary: masterstrict: falsedatasource:master:url: jdbc:postgresql://xxxxx:5432/dynamic-masterusername: xxxxpassword: xxxxdriver-class-name: org.postgresql.Driverdruid:initial-size: 1max-active: 20min-idle: 1max-wait: 6000pool-prepared-statements: truemax-pool-prepared-statement-per-connection-size: 20validation-query: select 1validation-query-timeout: 10
logging:config: classpath:log4j2.xml

3.4. 初始化数据源

新建DynamicDataSource配置类,将master库tenant表中数据源初始化

@Configuration
public class DynamicDataSource {@Value("${spring.datasource.dynamic.datasource.master.driver-class-name}")private String driverName;@Value("${spring.datasource.dynamic.datasource.master.url}")private String url;@Value("${spring.datasource.dynamic.datasource.master.username}")private String username;@Value("${spring.datasource.dynamic.datasource.master.password}")private String password;@Beanpublic DynamicDataSourceProvider dynamicDataSourceProvider() {return new AbstractJdbcDataSourceProvider(driverName, url, username, password) {@Overrideprotected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException {Map<String, DataSourceProperty> dataSourceMap = new HashMap<>();ResultSet resultSet = statement.executeQuery("select * from tenant");while (resultSet.next()) {String tenant = resultSet.getString("tenant_id");DataSourceProperty sourceProperty = new DataSourceProperty();sourceProperty.setDriverClassName(resultSet.getString("data_source_driver"));sourceProperty.setUrl(resultSet.getString("data_source_url"));sourceProperty.setUsername(resultSet.getString("data_source_username"));sourceProperty.setPassword(resultSet.getString("data_source_password"));dataSourceMap.put(tenant, sourceProperty);}return dataSourceMap;}};}
}

3.5. 存储当前数据源

因为每次请求需要访问的数据库可能都不一样,所以需要在每次请求操作时需要指定需要访问哪个数据库,新建一个拦截器

@Log4j2
public class DynamicDataSourceInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String headerTenant = request.getHeader("X-Tenant-Id");if (StringUtils.hasText(headerTenant)) {DynamicDataSourceContextHolder.push(headerTenant);return true;}writerMessage(response, ResponseEntity.status(HttpStatus.BAD_REQUEST).body("X-Tenant-Id in request header cannot be empty!"));log.warn("X-Tenant-Id in request header cannot be empty, The path is {}", request.getRequestURL());return false;}private void writerMessage(HttpServletResponse response, ResponseEntity<String> errorMessage) {try (PrintWriter writer = response.getWriter()) {response.setStatus(errorMessage.getStatusCodeValue());response.setCharacterEncoding("UTF-8");response.setContentType("text/html; charset=utf-8");writer.print(errorMessage.getBody());} catch (Exception e) {e.printStackTrace();}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {DynamicDataSourceContextHolder.clear();}
}

将自定义拦截器加入配置类,新建一个Web配置类

@Configuration
public class WebAutoConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(dynamicDataSourceInterceptor()).addPathPatterns("/**");}@Beanpublic DynamicDataSourceInterceptor dynamicDataSourceInterceptor() {return new DynamicDataSourceInterceptor();}
}

3.6. 编写数据源Controller

@RestController
@RequestMapping(value = "/datasource")
public class DataSourceController {@Autowiredprivate DataSource dataSource;@Autowiredprivate DefaultDataSourceCreator dataSourceCreator;@Autowiredprivate TenantService tenantService;@GetMapping(value = "/getAllDataSources")public Set<String> getAllDataSources() {DynamicRoutingDataSource routingDataSource = (DynamicRoutingDataSource) dataSource;return routingDataSource.getDataSources().keySet();}@PostMapping(value = "/addDataSource")public ResponseEntity<String> addDataSource(@RequestBody DataSourceDto dataSourceDto) {DataSourceProperty sourceProperty = TenantMapper.TENANT_MAPPER.dataSourceDtoToDataSourceProperty(dataSourceDto);DynamicRoutingDataSource routingDataSource = (DynamicRoutingDataSource) dataSource;DataSource propertyDataSource = dataSourceCreator.createDataSource(sourceProperty);routingDataSource.addDataSource(dataSourceDto.getTenantId(), propertyDataSource);Tenant tenant = TenantMapper.TENANT_MAPPER.dataSourceDtoToTenant(dataSourceDto);tenantService.saveOrUpdate(tenant);String dataSourceStr = routingDataSource.getDataSources().keySet().stream().collect(Collectors.joining(","));return ResponseEntity.ok(dataSourceStr);}
}

4. 测试

在postman中输入地址http://localhost:8000/datasource/getAllDataSources,在请求头新增X-Tenant-Id=master参数,发起GET请求
初始获取数据源信息
租户张三加入系统后,只需要为张三新建一个数据库,调用新增数据源接口就行,在postman中输入地址http://localhost:8000/datasource/addDataSource,发起POST请求
新增数据源
此时租户张三就可以查询自己的数据信息了,在postman中输入地址http://localhost:8000/tenant/customer/getCustomerInfo/:id,发起GET请求
查询张三信息
注意:请求头必须携带需要操作的数据源标识,否则会提出错误
无X-Tenant-Id访问
以上示例就简单实现了单体部署多租户系统的集成,如果是多实例部署是否有问题呢?

5. 多实例部署

5.1. 存在的问题

在Idea中同时启动两个实例8000和9000,8000服务新增租户李四数据源,分别查询8000服务和9000服务的数据源信息
8000服务数据源信息
再次查询9000服务数据源信息
9000服务数据源信息
对比发下在8000服务上新增了数据源,9000服务查询不到,且无法使用新增的数据源,这是因为服务一启动就将数据源信息初始化进了内存,8000服务和9000服务内存是相互独立的,故而8000服务上操作的数据无法同步到9000服务。如果将新增后的数据源存放到8000服务和9000服务都能访问到的第三方服务上,请求进入服务后执行前先对比本地内存数据源和远程服务数据源是否相等,若不等,就先将远程服务的数据源信息同步到本地内存,这样问题是不就解决了呢!

5.2. 同步数据源信息

本示例引入redis作为第三方服务,在拦截器中增加同步数据源的操作

@Log4j2
public class DynamicDataSourceInterceptor implements HandlerInterceptor {@Autowiredprivate TenantService tenantService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (StringUtils.hasText(headerTenant)) {tenantService.reloadDataSource();//其他代码省略...}}
}

同步数据源的代码如下:

public void reloadDataSource() {DynamicRoutingDataSource routingDataSource = (DynamicRoutingDataSource) dataSource;Set<String> dataSourceTypeSet = routingDataSource.getDataSources().keySet();String dataSourceType = dataSourceTypeSet.stream().collect(Collectors.joining(","));String redisDataSourceType = redisTemplate.opsForValue().get("dataSourceType");if (!dataSourceType.equals(redisDataSourceType)) {dataSourceTypeSet.stream().filter(sourceType -> !sourceType.equals("master")).forEach(routingDataSource::removeDataSource);List<Tenant> tenantList = this.list();tenantList.stream().filter(tenant -> !tenant.getTenantId().equals("master")).forEach(tenant -> {DataSourceProperty sourceProperty = new DataSourceProperty();sourceProperty.setDriverClassName(tenant.getDataSourceDriver());sourceProperty.setUrl(tenant.getDataSourceUrl());sourceProperty.setUsername(tenant.getDataSourceUsername());sourceProperty.setPoolName(tenant.getDataSourcePoolname());sourceProperty.setPassword(tenant.getDataSourcePassword());DataSource propertyDataSource = dataSourceCreator.createDataSource(sourceProperty);routingDataSource.addDataSource(tenant.getTenantId(), propertyDataSource);});redisTemplate.opsForValue().set("dataSourceType", tenantList.stream().map(tenant -> tenant.getTenantId()).collect(Collectors.joining(",")));}
}

同时需要在新增数据源的地方将数据源信息set进redis

@PostMapping(value = "/addDataSource")
public ResponseEntity<String> addDataSource(@RequestBody DataSourceDto dataSourceDto) {//其他代码省略....redisTemplate.opsForValue().set("dataSourceType", dataSourceStr);//......
}

重启两个示例,再次新增数据源和查询数据源信息
同步收查询9000服务数据源信息

后记

由于作者能力有限,文中难免会出现一些错误,欢迎各位大佬不吝赐教,也希望各位大佬就多实例部署如何同步数据源问题在评论处留言讨论

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

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

相关文章

手写一个IO泄露监测框架

作者&#xff1a;长安皈故里 大家好&#xff0c;最近由于项目原因&#xff0c;对IO资源泄漏的监测进行了一番调研深入了解&#xff0c;发现IO泄漏监测框架实现成本比较低&#xff0c;效果很显著&#xff1b;同时由于IO监测涉及到反射&#xff0c;还了解到了通过一种巧妙的方式实…

通达信欧奈尔RPS指标公式详解

RPS相对强度指标&#xff0c;是国内的投资者根据威廉欧奈尔所著书籍《笑傲股市》中的RS评级改进的。 根据书中介绍&#xff1a; RS评级衡量了某一给定股票在过去52周内相对股市中其他股票的表现。市场上每一只股票都被指定了1~99范围内的某一数值&#xff0c;99代表相对强度最高…

YOLOV7运行步骤(推理、训练全过程)

下载源代码&#xff1a;点击下载 执行以下命令安装requirements.txt中的相关依赖 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple官网下载权重yolov7.pt&#xff08;测试使用&#xff09;、yolov7-tiny.pt&#xff08;训练使用&#xff0c;这里…

10 JS01——初识JS

目标&#xff1a; 1、初识JavaScript 2、JavaScript注释 3、JavaScript输入输出语句 一、初识JavaScript 1、JavaScript是什么 JavaScript是世界上最流行的语言之一&#xff0c;是一种运行在客户端的脚本语言(Script是脚本的意思) 脚本语言:不需要编译&#xff0c;运行过程…

Vue2-黑马(九)

0目录&#xff1a; &#xff08;1&#xff09;router-动态菜单 &#xff08;2&#xff09;vuex-入门 &#xff08;3&#xff09;vuex-mapState &#xff08;1&#xff09;router-动态菜单 我们点击按钮跳转到主页面&#xff0c;主页在制作动态菜单&#xff0c;路由的跳转方…

keil代码格式化快捷方法

美化当前文件&#xff1a;-n !E --styleansi -p -s4 -S -f -xW -w -xw 美化整个工程文件&#xff1a;-n "$E*.c" "$E*.h" --styleansi -p -s4 -S -f -xW -w -xw -R 当前时间&#xff1a;!E~E^E 添加文件注释&#xff1a;!E 函数功能注释&#xff1a;!E ~…

快排(动图详细版,快速理解)

注&#xff1a;本文主要介绍六大排序中的快排 文章目录前言一、三大法则1.1 Hoare法1.2 挖坑法1.3 双指针法&#xff08;更加便捷&#xff09;1.4 三种方法时间复杂度计算二、快排栈问题优化方式2.1 三数取中2.2 小区间优化三、非递归快排前言 快速排序是Hoare于1962年提出的一…

Linux高并发服务器(webserver)

一.有限状态机 它的转移函数表示系统从一个状态转移到另一个状态的条件 二.EPOLL 在内核中创建一个数据&#xff0c;这个数据有两个比较重要的数据&#xff0c;一个是需要检测的文件描述符的信息&#xff08;红黑树&#xff09;&#xff0c;一个双向链表&#xff0c;存放检测到…

利用多专家模型解决长尾识别任务

来源&#xff1a;投稿 作者&#xff1a;TransforMe 编辑&#xff1a;学姐 贡献 提出了RoutIng Diverse Experts&#xff08;RIDE&#xff09;&#xff0c;不仅可以减少所有类别的variance&#xff0c;并且还可以减少尾部类的bias。同时提升了头部和尾部的性能。 思路 目前存…

easyrecovery2023电脑文件数据恢复软件功能介绍

EasyRecovery功能全面&#xff0c;即便是没有经验的小白用户也可以很快上手&#xff0c;让你足不出户即可搞定常见的数据丢失问题。 在使用和操作存储设备期间&#xff0c;数据丢失问题在所难免。比如&#xff0c;误删除某个文件、不小心将有数据的分区格式化、误清空了有重要…

2023“认证杯”数学中国数学建模赛题浅析

2023年认证杯”数学中国数学建模如期开赛&#xff0c;本次比赛与妈杯&#xff0c;泰迪杯时间有点冲突。因此&#xff0c;个人精力有限&#xff0c;有些不可避免地错误欢迎大家指出。为了大家更方便的选题&#xff0c;我将为大家对四道题目进行简要的解析&#xff0c;以方便大家…

【vue3】04-vue基础语法补充及阶段案例

文章目录vue基础语法补充vue的computedvue的watch侦听书籍购物车案例vue基础语法补充 vue的computed computed&#xff1a;用于声明要在组件实例上暴露的计算属性。&#xff08;官方文档描述&#xff09; 我们已经知道&#xff0c;在模板中可以直接通过插值语法显示一些data中…

智能网卡相关知识(smart nic 、DPU)

网卡作为穿行在网络与计算之间的桥梁&#xff0c;是可以解决计算瓶颈的关键硬件。 随着CPU 密度和数据中心网络带宽的进一步提升&#xff0c;用户对预期性能的需求&#xff0c;系统运行平稳性都会有更高的要求。云厂商一方面面临巨大的成本压力&#xff0c;另一方面面临巨大的…

新一代异步IO框架 io_uring | 得物技术

1.Linux IO 模型分类 相比于kernel bypass 模式需要结合具体的硬件支撑来讲&#xff0c;native IO是日常工作中接触到比较多的一种&#xff0c;其中同步IO在较长一段时间内被广泛使用&#xff0c;通常我们接触到的IO操作主要分为网络IO和存储IO。在大流量高并发的今天&#xff…

光伏电池片技术N型迭代,机器视觉检测赋能完成产量“弯道超车”

电池片是光伏发电的核心部件&#xff0c;其技术路线和工艺水平直接影响光伏组件的发电效率和使用寿命。随着硅料、硅片技术逐渐接近其升级迭代空间的瓶颈&#xff0c;电池片环节正处于技术变革期&#xff0c;是光伏产业链中迭代最快的部分。P型中PERC电池片是现阶段市场的主流产…

C/C++每日一练(20230413)

目录 1. 与浮点数A最接近的分数B/C &#x1f31f; 2. 比较版本号 &#x1f31f;&#x1f31f; 3. 无重复字符的最长子串 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每…

Multi-modal Alignment using Representation Codebook

Multi-modal Alignment using Representation Codebook 题目Multi-modal Alignment using Representation Codebook译题使用表示代码集的多模态对齐期刊/会议CVPR 摘要&#xff1a;对齐来自不同模态的信号是视觉语言表征学习&#xff08;representation learning&#xff09;的…

Vue2_02_指令

模板语法 — Vue.js (vuejs.org) 指令 (Directives) 是带有 v- 前缀的特殊 attribute 参数 一些指令能够接收一个“参数”&#xff0c;在指令名称之后以冒号表示 <a v-bind:href"url">...</a> 动态参数 可以用方括号括起来的 JavaScript 表达式作为一…

JWT与Token详解

前言&#xff1a;JWT全称“JSON Web Token”&#xff0c;是实现Token的机制。官网&#xff1a;https://jwt.io/ JWT的应用 JWT用于登录身份验证。用户登录成功后&#xff0c;后端通过JWT机制生成一个token&#xff0c;返回给客户端。客户端后续的每次请求都需要携带token&…

常用加密算法

目录 常见的加密算法可以分成三种&#xff1a; 对称加密算法 DES 3DES AES 非对称加密 RSA ECC Hash算法 MD5 SHA1 算法对比 算法选择 常见的加密算法可以分成三种&#xff1a; 对称加密算法&#xff1b;非对称加密算法&#xff1b;Hash算法&#xff1b;接下来我们…