SaaS化多租户实现的两种方法

news2024/11/19 17:22:00

SaaS化多租户实现的两种方法

SaaS系统的定义 SaaS,全称为Software-as-a-Service(软件即服务),是一种基于云计算的软件交付模式。而SaaS系统,即是通过这种模式提供给用户的软件系统。即多租户系统,每个租户独立,只能看到自己数据。

一、租户id隔离

这种方法比较简单,在每张表里添加一个字段tenant_id,给每个企业(租户)一个唯一tenant_id,那么在SQL的一切增删改查都带上tenant_id,即可实现租户隔离。

如何自动带上租户id,无需每次都在sql上添加tenant_id?
使用mybatis-plugin可以做到

步骤一、写一个拦截器

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,
                Integer.class})
})
public class CustomerInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //todo 拦截逻辑
        System.out.println("");

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        String originalSql = statementHandler.getBoundSql().getSql();
        //实际开发中从登录用户去获取他的tenant_id 
        String modifiedSql = originalSql + " AND tenant_id = '" + tenant_id + "'";
        ReflectUtil.setFieldValue(statementHandler.getBoundSql(), "sql", modifiedSql);

        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }
    @Override
    public void setProperties(Properties properties) {
        //设置属性
    }
}

步骤二、注册插件

@Configuration
public class MybatisConfig {
    @Bean
    public String myInterceptor(SqlSessionFactory sqlSessionFactory) {
        sqlSessionFactory.getConfiguration().addInterceptor(new CustomerInterceptor());
        return "interceptor";
    }
}

二、动态数据源(重点)

本文重点要讲的是使用动态数据源实现动态切换数据库,来实现多租户自由切换
本文使用的是mybatis-flex

步骤一、注册租户和数据源到数据库

即,把租户的唯一信息和分配给租户的数据源一一对应,存入数据库,例如:

CREATE TABLE `datasource` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `display_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示名称',
  `db_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '链接默认数据库',
  `schema_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数据库schema',
  `pool_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '连接池名称必须唯一',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '描述',
  `db_host` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据库地址',
  `db_port` int NOT NULL COMMENT '数据库端口',
  `db_user` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户',
  `db_password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
  `db_driver` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '驱动',
  `connect_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '连接参数',
  `create_time` timestamp NULL DEFAULT NULL,
  `update_time` timestamp NULL DEFAULT NULL,
  `create_by` timestamp NULL DEFAULT NULL,
  `update_by` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `datasource_pool_name_uindex` (`pool_name`) USING BTREE,
  KEY `database_creator_id_index` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=159 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

示例数据:
在这里插入图片描述

步骤二、程序启动完成把数据连接信息加载到JVM
@Component
@Order(1)
public class InitialDataSource implements CommandLineRunner {

    public static final String DATASOURCE_MYSQL_COMMON_PARAMS_URL = "jdbc:%s://%s:%s/%s?%s";
    public static final String DATASOURCE_PGSQL_COMMON_PARAMS_URL = "jdbc:%s://%s:%s/%s?%s&%s";
	//这是数据源(步骤1提到的)表的mapper接口
    @Resource
    private DatasourceMapper datasourceMapper;

    @Override
    public void run(String... args) throws Exception {
        //1.清空内存中的数据源
        DataSourceKey.clear();
        //2.把数据库的datasource查询出来
        List<Datasource> fillSubmittals = datasourceMapper.selectAll();
        //3.动态添加新的数据源 FlexDataSource来自于mybatis-flex
        FlexDataSource flexDataSource = FlexGlobalConfig.getDefaultConfig().getDataSource();
        fillSubmittals.forEach(item -> addDatasourceItem(flexDataSource, item));
    }

    /**
     * 组装-添加数据源
     *
     * @param flexDataSource
     * @param item
     */
    public void addDatasourceItem(FlexDataSource flexDataSource, Datasource item) {

        DruidDataSource druidDataSource = buildDruidDataSource(item);
        //数据源信息加载到内存
        addIntoJVMDynamicPool(flexDataSource, item.getPoolName(),druidDataSource);

    }

    /**
     * 数据源信息加载到内存
     * @param flexDataSource
     * @param poolName
     * @param druidDataSource
     * @return
     */
    public void addIntoJVMDynamicPool(FlexDataSource flexDataSource, String poolName, DruidDataSource druidDataSource){
        flexDataSource.addDataSource(poolName,druidDataSource);
    }

    public DruidDataSource buildDruidDataSource(Datasource item){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(getCommonUrl(item));
        druidDataSource.setDriverClassName(DriverEnum.findByEnumDescription(item.getDbDriver()).getDriverClass());
        druidDataSource.setUsername(item.getDbUser());
        druidDataSource.setPassword(item.getDbPassword());
        druidDataSource.setValidationQuery("select 1");
        return druidDataSource;
    }

    public String getCommonUrl(Datasource datasource){
        String url = null;
        if(DriverEnum.MYSQL.getDescription().equals(datasource.getDbDriver())){

             url = String.format(DATASOURCE_MYSQL_COMMON_PARAMS_URL,
                    datasource.getDbDriver(),
                    datasource.getDbHost(),
                    datasource.getDbPort(),
                    StringUtils.hasText(datasource.getDbName()) ? datasource.getDbName() : "",
                    StringUtils.hasText(datasource.getConnectParams()) ? datasource.getConnectParams() : "");

        }else if(DriverEnum.POSTGRES.getDescription().equals(datasource.getDbDriver())){

            url =String.format(DATASOURCE_PGSQL_COMMON_PARAMS_URL,
                    datasource.getDbDriver(),
                    datasource.getDbHost(),
                    datasource.getDbPort(),
                    StringUtils.hasText(datasource.getDbName()) ? datasource.getDbName() : "",
                    StringUtils.hasText(datasource.getSchemaName()) ? "currentSchema="+datasource.getSchemaName() : "",
                    StringUtils.hasText(datasource.getConnectParams()) ? datasource.getConnectParams() : "");

        }
        return url;
    }

}

用到的枚举:

@Getter
public enum DriverEnum {
    /**
     * pg
     */
    POSTGRES(0, "postgresql", "org.postgresql.Driver", DbType.postgresql, "postgres"),
    /**
     * mysql
     */
    MYSQL(1, "mysql", "com.mysql.cj.jdbc.Driver", DbType.mysql,"mysql"),

    /**
     * ck
     */
    CLICK_HOUSE(2, "clickhouse", "com.clickhouse.jdbc.ClickHouseDriver", DbType.clickhouse, "clickhouse");

    private final int index;
    // bi 记录驱动
    private final String description;
    private final String driverClass;
    // metabase 记录驱动
    private final String engine;

    private final DbType analysisType;

    DriverEnum(int index, String description, String driverClass, DbType analysisType, String engine){
        this.index = index;
        this.description = description;
        this.driverClass = driverClass;
        this.analysisType = analysisType;
        this.engine = engine;
    }

    private static final Map<String, DriverEnum> DESCRIPTION_ENUMS_MAP = Map.of(
            DriverEnum.POSTGRES.getDescription(), DriverEnum.POSTGRES,
            DriverEnum.MYSQL.getDescription(), DriverEnum.MYSQL,
            DriverEnum.CLICK_HOUSE.getDescription(), DriverEnum.CLICK_HOUSE
    );

    private static final Map<String, DbType> ANALYSISTYPE_ENUMS_MAP = Map.of(
            DriverEnum.POSTGRES.getDescription(), DbType.postgresql,
            DriverEnum.MYSQL.getDescription(), DbType.mysql,
            DriverEnum.CLICK_HOUSE.getDescription(), DbType.clickhouse
    );


    /**
     * 判断参数合法性
     */
    public static boolean isValidName(String name) {
        for (DriverEnum cardStatus : DriverEnum.values()) {
            if (cardStatus.getDescription().equals(name)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 根据描述查找枚举
     * @param description 描述
     * @return 枚举
     */
    public static DriverEnum findByEnumDescription(String description){
        return DESCRIPTION_ENUMS_MAP.getOrDefault(description, DriverEnum.POSTGRES);
    }

    /**
     * 根据描述查找SQL解析器
     * @param description 描述
     * @return 枚举
     */
    public static DbType findAnalysisTypeByDescription(String description){
        return ANALYSISTYPE_ENUMS_MAP.getOrDefault(description, DbType.postgresql);
    }
}
步骤三、业务使用(只列出核心)

@Resource
    private JdbcTemplate jdbcTemplate;
    
 public List<FillSubmittal> queryAll() {

        String sql1 = "select * from datasource";
        //设置数据库pool-name   与步骤一的表里的pool_name对应
        DataSourceKey.use("c-1");
        executeSql(sql1);
        
        String sql2 = "select * from nc_fill_table_24_4lg0aa20f4rw9r";
        DataSourceKey.use("c-2");
        executeSql(sql2);

        String sql3 = "select * from user_info";
        DataSourceKey.use("c-3");
        executeSql(sql3);

        String sql4 = "select * from sys_role";
        DataSourceKey.use("c-4");
        executeSql(sql4);
        return null;
    }
 public void executeSql(String sql){
        List<Map<String, Object>> list =jdbcTemplate.queryForList(sql);
        log.info(JSON.toJSONString(list));
    }

这样,每个租户注册的时候就分配一个数据源,在使用时,根据租户灯笼裤信息获取到他对应的数据源信息,就可以通过DataSourceKey.use设置当前租户要用的数据源,实现动态切换


如图: 这里只画了程序启动,把数据加载到JVM中,和使用时,根据不用的pool_name(与租户一一对应)切换到对应的数据源。那么执行sql得到的就是对应数据源的数据,注意:这里执行sql使用的是jdbcTemplate了

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL 日志篇:Redo 文件和自适应检查点

MySQL 的 InnoDB 存储引擎使用 Redo Log 记录事务对数据的更改&#xff0c;以便在系统崩溃恢复时能够重做这些更改&#xff0c;从而保证事务的持久性。对于产生的 Redo Log&#xff0c;InnoDB 存储引擎首先将其写入内存中的 Log Buffer&#xff0c;随后再将 Log Buffer 中的 Re…

力扣337-打家劫舍 III(Java详细题解)

题目链接&#xff1a;337. 打家劫舍 III - 力扣&#xff08;LeetCode&#xff09; 前情提要&#xff1a; 本体是打家劫舍的一个变形题&#xff0c;希望大家能先做198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09;&#xff0c;并看一下我上题的讲解力扣198-打家劫舍&…

【刷题】Day 3--错误的集合

hello&#xff01;又见面啦~~~ 一道习题&#xff0c;要长脑子了...... 【. - 力扣&#xff08;LeetCode&#xff09;】 【思路】 /*** Note: The returned array must be malloced, assume caller calls free().*/void Bubble_sort(int arr[], int size) {int temp;for (int i…

多速率信号处理-CIC滤波器

基本原理 级联积分梳状滤波器&#xff08;Cascade Intergrator Comb&#xff09;是多速率信号处理中一种十分高效的数字滤波器。CIC滤波器具有低通滤波器的特性&#xff0c;同时具有以下优势&#xff1a; 滤波器系数全为1&#xff0c;设计时不需要存储滤波器系数&#xff0c;…

拖放WORD文件朗读全文

把WORD拖放到tkinter的窗口&#xff0c;就可以朗读整改word文件的内容。 代码&#xff1a; # -*- coding: utf-8 -*- """ Created on Tue Sep 10 17:09:35 2024author: YBK """ import pyttsx3 import comtypes.client import os import tkint…

按包边(边框)尺寸分类异形创意圆形(饼/盘)LED显示屏有哪些种类

在LED显示屏技术日新月异的今天&#xff0c;异形创意圆形&#xff08;饼/盘&#xff09;LED显示屏凭借其独特的形态设计与广泛的应用场景&#xff0c;成为了商业展示、舞台表演、艺术装置以及户外广告等领域的宠儿。其中&#xff0c;按包边&#xff08;边框&#xff09;尺寸的不…

holynix靶机详解

靶机配置 加一个网络适配器&#xff08;网卡&#xff09; 修改MAC地址 00:0C:29:BC:05:DE 原来的网卡设置为桥接&#xff0c;随机生成MAC地址 重启靶机即可扫到靶机IP 主机探测与端口扫描 arp-scan -l 发现开放80端口 nmap -sV -A -T4 192.168.229.153 访问网站 http://1…

OpenAI O1:人工智能推理能力的新里程碑

引言 北京时间9月13日凌晨&#xff0c;OpenAI在没有任何预告的情况下&#xff0c;正式发布了其首款具有推理能力的模型——OpenAI O1。这一模型的发布&#xff0c;不仅标志着人工智能能力的新水平&#xff0c;也预示着AI技术发展的新范式。本文将详细解析OpenAI O1模型的技术特…

【计网】数据链路层:概述之位置|地位|链路|数据链路|帧

✨ Blog’s 主页: 白乐天_ξ( ✿&#xff1e;◡❛) &#x1f308; 个人Motto&#xff1a;他强任他强&#xff0c;清风拂山岗&#xff01; &#x1f4ab; 欢迎来到我的学习笔记&#xff01; ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ 1. 在OSI体系结构中的位置 1. 位置&#xff1a;数…

每日一练:K个一组翻转链表

25. K 个一组翻转链表 - 力扣&#xff08;LeetCode&#xff09; 一、题目要求 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&#xff0c;它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍&#x…

时间复杂度计算 递归

我们先拿出 2021 csp-s 程序题中一道看着就头大的程序题&#xff0c;要求分析 solve1 的复杂度。 设 T(n) ⁡ \operatorname{T(n)} T(n) 表示数组长度为 n n n 时的复杂度&#xff08;即 m − h 1 n m-h1n m−h1n&#xff09;。 T ( 1 ) 1 T(1)1 T(1)1&#xff0c;根据…

计算机毕业设计 酷听音乐系统的设计与实现 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

【SQL】百题计划:SQL排序Order by的使用。

简述&#xff1a; 排序函数&#xff1a;Order by&#xff1b;升序 ASC&#xff1b;降序 DESC&#xff1b; 答案&#xff1a; Select distinct author_id as id from Views where author_id viewer_id order by id Asc;

关于华大/小华 HC32F460 在IAR环境中,无法启用FPU 硬件浮点运算单元的解决方案

需求&#xff1a;要使用浮点FFT功能&#xff0c;面开启M4的 FPU功能 问题&#xff1a;无法开启 FPU&#xff0c;如下图所示&#xff1a;此栏为灰色&#xff0c;无法选择 尝试强制增加 __ARMVFP__&#xff1a; 编译出错&#xff0c;无法内链FPU&#xff1a; 解决方案&#xff1…

[000-01-008].第05节:OpenFeign高级特性-日志打印功能

我的后端学习大纲 SpringCloud学习大纲 1、日志打印功能&#xff1a; 1.Feign 提供了日志打印功能&#xff0c;我们可以通过配置来调整日志级别&#xff0c;从而了解 Feign 中 Http 请求的细节&#xff0c;说白了就是对Feign接口的调用情况进行监控和输出 2、日志级别: NONE&…

vue3【实战-组件封装】图文卡片

效果预览 技术要点 图片宽高比固定为 16:9&#xff0c;展示方式为 object-fit: cover通过 v-bind 实现父组件向子组件的批量传参单行文本超长显示省略号 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; title 属性实现鼠标悬浮显示文本完整内容 范例代码 …

HarmonyOS开发之使用Picker(从相册选择图片),并且通过Swiper组件实现图片预览

一&#xff1a;效果图&#xff1a; 二&#xff1a;添加依赖 import picker from ohos.file.picker; 三&#xff1a;创建showDialog showDialog() {AlertDialog.show({message: 从相册选择,alignment: DialogAlignment.Bottom,offset: { dx: 0, dy: -12 },primaryButton: {val…

Java面试、技巧、问题、回复,资源面面观

入门 先了解一下面试流程 复习 Java 基础知识&#xff1a; 温习 Java 编程的核心概念&#xff0c;包括数据类型、变量、循环、数组和面向对象的编程原则。数据结构和算法&#xff1a; 加强您对 Java 编程中使用的基本数据结构和算法的理解。练习编码&#xff1a; 在各种平台上解…

PHP一键约课高效健身智能健身管理系统小程序源码

一键约课&#xff0c;高效健身 —— 智能健身管理系统让健康触手可及 &#x1f3cb;️‍♀️ 告别繁琐&#xff0c;一键开启健身之旅 你还在为每次去健身房前的繁琐预约流程而烦恼吗&#xff1f;现在有了“一键约课高效健身智能健身管理系统”&#xff0c;所有问题都迎刃而解…

YARN----调度策略

Yarn中&#xff0c;负责给应用分配资源的就是Scheduler 在Yarn中有三种调度器可以选择&#xff1a;FIFO Scheduler &#xff0c;Capacity Scheduler&#xff0c;Fair Scheduler FIFO Scheduler 先进先出策略 在进行资源分配的时候&#xff0c;先给队列中最先上的应用进行分配…