本文正在参加「金石计划」

前言

今日来了解下怎么运用 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 体系。

关于用户来说,不需求关心技术问题,只要用你供给的服务就行。

  • 长处:便利便捷,能够有用的对资源进行利用,用户能够直接运用并且管理这些软件发生的数据就能够了,而且能够按需运用,挑选需求功用付费不付费都行。能够有多个用户或许企业用户存在。
  • 缺陷:用户数据在云端,自己不能完全有用的把握

总结:

管理系统必备技(14):Mybatis-plus 实现多租户业务实战

  • 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

管理系统必备技(14):Mybatis-plus 实现多租户业务实战

而 tenant 表的该字段就会主动填充上。

管理系统必备技(14):Mybatis-plus 实现多租户业务实战

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

管理系统必备技(14):Mybatis-plus 实现多租户业务实战

数据库中也加上了对应的值

管理系统必备技(14):Mybatis-plus 实现多租户业务实战

总结:以上便是有关 mybatis-plus 的多租户的实践运用,纯手打,感谢阅览