本文正在参加「金石计划」
前言
今日来了解下怎么运用 mybatis-plus 完成咱们的多租户实战,从多租户概念引入到SpringBoot项目中的实践运用。这块也之前现已在项目中运用,将这部分功用摘取出来进行demo 演示。
一、多租户概念
1.1 云服务形式
要想了解下多租户的概念,咱们需求了解下几种云服务形式,常见的有 IAAS、PAAS、SAAS 等服务。
而咱们的多租户是 SAAS 服务特有的产物。SAAS 服务是布置在云端,客户能够同时运用同一套体系。
1.1.1 IAAS
含义为 Infrastructure as a server。即基础设施便是服务,意思便是把客户需求的基础设施环境建立好后,然后敞开虚拟机或硬件的租借服务,顾客不管理或操控任何云计算基础设施,但能操控操作体系的挑选、存储空间、布置的运用,也有或许取得有约束的网络组件(例如路由器、防火墙、负载均衡器等)的操控。
一般运用云服务器便是这样。
- 长处:IAAS 的自由度、灵活度十分高,用户能够自行装置操作体系、数据库及各类软件。
- 缺陷:保护本钱比较高,cpu、内存等资源跑不满或许会浪费,需求投入运维本钱。
1.1.2 PAAS
渠道及服务,在云端把客户所需的软件等环境整合的渠道出租给用户,进行收费。
云厂商现已给大家建立好了渠道,这个渠道出租给你一个空间,这个云端空间里面现已装好了各类所需的软件,比方操作体系、云数据库、云中间件、网关、云负载均衡器等相关的内容。
- PAAS 长处:削减建立环境的各种本钱,用户能够削减资源。
- 缺陷:自由度和灵活度很低。
小结:
- 其实咱们平时运用云服务器,大多数是选用 IAAS+PASS 相互结合。
1.2.3 SAAS
软件即服务,也便是多用户的 web 体系。
关于用户来说,不需求关心技术问题,只要用你供给的服务就行。
- 长处:便利便捷,能够有用的对资源进行利用,用户能够直接运用并且管理这些软件发生的数据就能够了,而且能够按需运用,挑选需求功用付费不付费都行。能够有多个用户或许企业用户存在。
- 缺陷:用户数据在云端,自己不能完全有用的把握
总结:
- IaaS,是供给最底层的服务,由于最接近服务器硬件资源,这样用户能够以最大的自由度接入构建网络以及服务器装备;
- PaaS,是供给了更高一层的服务。整体服务并没有向用户展示底层网络与硬件资源,整个底层是透明的,直接向用户敞开云端产品软件以及开发运转环境;
- SaaS,供给最上层服务。关于用户来说最简略,所见即所得,不需求技术开发人员也能够拥有自己的一套软件。
1.2 多租户 VS 单租户
何为多租户?说到租户,就来说说租房子。
二房东将房子租来后,进行装修、将房子分隔成 5 个间隔间,然后将每个间隔间的用户出租给张三、李四、王五… ,而这些租户他们是合租的,对方的房间他们进不去,这便是保证了各自的私密性,也便是数据阻隔。
可是,关于公共区域是能够随时进入的,比方客厅、卫生间、厨房,这些数据便是共享数据,大家都能够访问,比方说其实便是个多租户渠道,关于共享的小册、活动大家都能看到,而关于创作者自身的数据便是只能通过用户自己的 id 自己检查。
所以,其实关于SAAS多租户体系,要比单一体系来的更加节省硬件资源,由于咱们只需求布置一套体系就能够了,一切的硬件设备也只需求采购一次。可是相对来说,咱们不能为企业供给定制化的需求方案,关于特定的要求,多租户欠好去满意,可是一般来说,咱们能够搜集各 方需求,去把各个租户的需求整合,然后根据收取不同的费用,供给可选的软件服务即可,那么这个便是saas的表现。
而单租户便是整租的概念,一切设施都是自己在用,定制化要求高,同时关于互联网来说,很多老体系仍是单租户,每年的保护费用也是十分高的,给不同的企业进行定制化开发和布置。
不难看出,多租户一定是 SAAS 形式,由于软件为租户供给了服务。SAAS 纷歧定是多租户。
多租户 | 单租户 | |
---|---|---|
数据阻隔性 | 低 | 高 |
数据共享性 | 高 | 低 |
数据库复杂度 | 高 | 低 |
可定制化度 | 低 | 高 |
版本迭代 | 简略 | 复杂 |
硬件本钱 | 低 | 高 |
1.3 设计方案
针对多租户,一般有三种设计方案:
- 独立数据库:
-
- 也便是一个租户一个数据库,这种级别和单租户是差不多
- 适合数据阻隔性很高企业:医院、金融
- 同一数据库,不同的 schema
- 相同数据库,同一张表(用的最多,本文选用该方式)
-
- 阻隔性最低,本钱最低
二、Mybatis-plus多租户实战
2.1 环境建立
1、引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
2、创建两张测验表(内容一致)
CREATE TABLE `tenant` (
`id` int NOT NULL AUTO_INCREMENT,
`email` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
`account` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
`tenant_id` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `tenant_2` (
`id` int NOT NULL AUTO_INCREMENT,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`account` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`tenant_id` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
3、新建对应实体和 mapper 方法
@Data
@TableName("tenant")
public class TestTenant {
@TableId(type = IdType.AUTO)
private Integer id;
private String account;
private String email;
private Integer tenantId;
}
@Data
@TableName("tenant_2")
public class TestTenant2 {
@TableId(type = IdType.AUTO)
private Integer id;
private String account;
private String email;
private Integer tenantId;
}
2.2 mybaits装备
动态传入疏忽的表和自定义化的多租户列名
/**
* 多租户设置
* @Author xiaolei
* @Date 2023/3/24 10:11
**/
public class TenantConfig extends TenantLineInnerInterceptor {
public static ThreadLocal<String> tenantContext = new ThreadLocal();
/**
* 多租户初始化设置
* @param ignoreTable 疏忽的表
* @param idName 租户id
*/
public TenantConfig(final List<String> ignoreTable, final String idName) {
super(new TenantLineHandler() {
// 获取租户id ,若没有则传入 -1
@Override
public Expression getTenantId() {
String tenantId = (String)TenantConfig.tenantContext.get();
return new LongValue(tenantId == null ? "-1" : tenantId);
}
// 疏忽表,true 表明疏忽该表
@Override
public boolean ignoreTable(String tableName) {
return ignoreTable.contains(tableName);
}
// 多租户的列名自定义
@Override
public String getTenantIdColumn() {
return idName;
}
});
}
}
/**
* @Author xiaolei
* @Date 2023/2/21 15:20
**/
@Configuration
public class MybatisPlusConfig {
protected final Log log = LogFactory.getLog(this.getClass());
@Value("${spring.datasource.tenant.ignoreTable:''}")
private List<String> ignoreTable;
@Value("${spring.datasource.tenant.idName:tenant_id}")
private String idName;
@Value("${spring.datasource.tenant.enable:true}")
private Boolean enable;
/**
* 新多租户插件装备,一缓和二缓遵从mybatis的规则,需求设置 MybatisConfiguration#useDeprecatedExecutor = false 防止缓存万一出现问题
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
this.log.debug("租户ID " + this.getIdName());
this.log.debug("疏忽表 " + String.join(",", this.getIgnoreTable()));
if(this.getEnable()){
interceptor.addInnerInterceptor(new TenantConfig(this.getIgnoreTable(),this.idName));
te
2.3 装备文件
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx:3306/lottery?characterEncoding=utf-8&useSSL=false
username: xxxx
password: xxxx
tenant:
enable: true // 是否开启多租户
idName: tenant_id
ignoreTable: tenant_2 // 逗号拼接
2.4 测验
偷闲直接在 controller 层写了。
@RestController
@RequestMapping("/ten")
public class TenantController {
@Autowired
TenantMapper tenantMapper;
@Autowired
TenantMapper2 tenantMapper2;
@GetMapping("v1/{id}")
public String insertUser(@PathVariable String id){
TenantConfig.tenantContext.set(id);
System.out.println(TenantConfig.tenantContext.get());
TestTenant testTenant = new TestTenant();
testTenant.setEmail("123@qq");
testTenant.setAccount("xiaolei");
tenantMapper.insert(testTenant);
return "success";
}
@GetMapping("v2/{id}")
public String insertUser2(@PathVariable String id){
TenantConfig.tenantContext.set(id);
System.out.println(TenantConfig.tenantContext.get());
TestTenant2 testTenant = new TestTenant2();
testTenant.setEmail("123@qq");
testTenant.setAccount("xiaolei");
tenantMapper2.insert(testTenant);
return "success";
}
}
开始访问两个版本的接口,能够发现tenant_2 该表的多租户没有设置上该id
而 tenant 表的该字段就会主动填充上。
2.5 AOP 阻拦优化
前面也看到,咱们不或许每次都会履行 TenantConfig.tenantContext.set(id); 这对咱们来说太繁琐了,而需求做的便是通过 AOP 阻拦得到用户的租户id,将其主动赋值上,关于咱们业务来说便是无感知的,这一部分做好了,咱们的多租户也就完成了。因而,在 AOP 中能够对用户的身份进行获取,能够从恳求头中获取,这块根据自己业务来完成。
这块测验就从常量中获取该用户id。
@Aspect
@Component
@Slf4j
public class CompanyAspect {
static String redisUserId = "100";
private static final String ignoreUrl = "/login";
@Around("execution(* com.xiaolei.*.controller..*.*(..))")
public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {
// 1.方法履行前的处理,相当于前置告诉
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Map<String,Object> joinPointInfo=getJoinPointInfoMap(joinPoint);
Object obj = joinPointInfo.get("paramMap");
if(!request.getRequestURI().equalsIgnoreCase(ignoreUrl)){
if(obj!=null){
TenantConfig.tenantContext.set(redisUserId);
}
}
Object proceed = joinPoint.proceed();
TenantConfig.tenantContext.remove();
return proceed;
}
private static Map<String, Object> getJoinPointInfoMap(JoinPoint joinPoint) {
Map<String,Object> joinPointInfo=new HashMap<>();
String classPath=joinPoint.getTarget().getClass().getName();
String methodName=joinPoint.getSignature().getName();
joinPointInfo.put("classPath",classPath);
Class<?> clazz=null;
CtMethod ctMethod=null;
LocalVariableAttribute attr=null;
int length=0;
int pos = 0;
try {
clazz = Class.forName(classPath);
String clazzName=clazz.getName();
ClassPool pool=ClassPool.getDefault();
ClassClassPath classClassPath=new ClassClassPath(clazz);
pool.insertClassPath(classClassPath);
CtClass ctClass=pool.get(clazzName);
ctMethod=ctClass.getDeclaredMethod(methodName);
MethodInfo methodInfo=ctMethod.getMethodInfo();
CodeAttribute codeAttribute=methodInfo.getCodeAttribute();
attr=(LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
if(attr==null){
return joinPointInfo;
}
length=ctMethod.getParameterTypes().length;
pos= Modifier.isStatic(ctMethod.getModifiers())?0:1;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
}
Map<String,Object> paramMap=new HashMap<>();
Object[] paramsArgsValues=joinPoint.getArgs();
String[] paramsArgsNames=new String[length];
for (int i=0;i<length;i++){
paramsArgsNames[i]=attr.variableName(i+pos);
String paramsArgsName=attr.variableName(i+pos);
Object paramsArgsValue = paramsArgsValues[i];
paramMap.put(paramsArgsName,paramsArgsValue);
joinPointInfo.put("paramMap", JSON.toJSONString(paramsArgsValue));
}
return joinPointInfo;
}
}
咱们能够把前面的 TenantConfig.tenantContext.set(id); 注释掉,然后通过署理后,就会给咱们主动拼接上具体用户的id。注意要把 ThreadLocal 清除。
开启 mybatis-plus日志,能够看到插入的够都默许拼接上了100
数据库中也加上了对应的值
总结:以上便是有关 mybatis-plus 的多租户的实践运用,纯手打,感谢阅览