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

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.mfbz.cn/a/875561.html

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

相关文章

Leetcode Hot 100刷题记录 -Day15(螺旋矩阵)

螺旋矩阵 问题描述&#xff1a; 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]]输出&#xff1a;[1,2,3,6,9,8,7,4,5] 示例 2&#xff1a; 输…

WebGL系列教程七(二维及三维旋转、平移、缩放)

目录 1 前言2 二维2.1 平移2.2 旋转2.3 缩放 3 三维3.1 平移3.2 旋转3.2.1 绕 X X X 轴旋转3.2.2 绕 Y Y Y 轴旋转3.2.3 绕 Z Z Z 轴旋转3.2.4 绕任意轴旋转 3.3 缩放 4 WebGL中怎么实现旋转、平移、缩放4.1 声明顶点着色器和片元着色器4.2 计算旋转矩阵4.3 绘制立方体并进行…

基于Matlab的模拟答题卡识别阅卷可以识别指定答题卡的各个部分-界面

识别指定答题卡的各个部分-界面-如学号&#xff0c;准考证号&#xff0c;客观题答案&#xff0c;主观题分数等用户可以在Excel中自行设置标准答案&#xff0c;并对六十题客观题进行批改&#xff0c;并显示分数。 项目介绍 本项目旨在开发一个基于MATLAB的答题卡识别阅卷系统&a…

认识泛型和包装类

认识泛型和包装类 包装类基本数据类型和对应的包装类装箱和拆箱自动装箱和自动拆箱 什么是泛型引出泛型语法 泛型类的使用语法示例类型推导 裸类型(Raw Type)说明 泛型如何编译的擦除机制 泛型的上界语法示例复杂示例 泛型方法定义方法示例使用类型推导和不用类型推导静态的泛型…

IO模型---BIO、NIO、IO多路复用、AIO详解

本篇将想给详细解释一下什么是BIO、NIO、IO多路复用以及AIO~ 同步的阻塞(BIO)和非阻塞(NIO)的区别 BIO&#xff1a;线程发来IO请求后&#xff0c;一直阻塞着IO线程&#xff0c;需要缓冲区这边数据准备好之后&#xff0c;才会进行下一步的操作。 举个&#x1f330;&#xff1…

UE5学习笔记21-武器的射击功能

一、创建C类 创建武器子弹的类&#xff0c;创建生产武器子弹的类&#xff0c;创建弹壳的类&#xff0c;生产武器子弹的类的父类是武器的类 创建后如图&#xff0c;ProjectileMyWeapon类(产生子弹的类)继承自weapon类&#xff0c;Projectile(子弹的类)&#xff0c;Casing(弹壳声…

如何使用QT完成记事本程序的UI界面布局

每日QT技巧查询表-CSDN博客 会持续更新记事本编写的全部过程&#xff0c;关注不迷路 一、相关控件 ①水平和垂直布局 ②按键 ③文本框 ④水平弹簧 ⑤标签 ⑥Widget 二、控件使用方法 1、PushButton 拖出三个按键&#xff0c;并对其进行命名&#xff0c;两处地方命名可以不一…

【Echarts】vue3打开echarts的正确方式

ECharts 是一个功能强大、灵活易用的数据可视化工具&#xff0c;适用于商业报表、数据分析、科研教育等多种场景。那么该如何优雅的使用Echarts呢? 这里以vue3为例。 安装echarts pnpm i echarts封装公用方法 // ts-nocheck import * as echarts from echarts; // 我们这里借…

C++:入门基础

一.C参考文档 https://legacy.cplusplus.com/reference/ https://zh.cppreference.com/w/cpp https://en.cppreference.com/w/ 二.C的第一个程序 #include <iostream> using namespace std;int main() {cout << "Hello world!" << en…

无人机PX4飞控ROS应用层开发:MAVROS 功能包介绍与飞控消息汇总(一)

概述 这个软件包提供了针对各种自动驾驶仪(如PX4,Ardupilot)使用 MAVLink 通信协议的通信驱动程序。 此外&#xff0c;它还提供了用于地面控制站&#xff08;例如 QGroundControl&#xff09;的 UDP MAVLink 桥接功能。 通常与PX4的offboard模式联合使用 Offboard控制背后的想…

单机docker-compose部署minio

单机多副本docker-compose部署minio 简单介绍 如果服务器有限可以单机挂载多硬盘实现多副本容错&#xff08;生产不推荐&#xff09; 部署好的文件状态 有两个重要文件 docker-compose.yaml和nginx.conf docker-compose.yaml是docker部署容器的配置信息包括4个minio和1个ng…

c中 int 和 unsigned int

c语言中&#xff0c;char、short、int、int64以及unsigned char、unsigned short、unsigned int、unsigned int64等等类型都可以表示整数。但是他们表示整数的位数不同&#xff0c;比如&#xff1a;char/unisigned char表示8位整数&#xff1b; short/unsigned short表示16位整…

菜鸟入门Docker

初始Docker Docker的概念 Docker的用途 DOcke的安装 Docker架构 配置Docker镜像加速器 Docker常用命令 Docker服务相关的命令。 Docker镜像相关的命令 Docker容器相关的命令 容器的数据卷 数据卷的概念和作用 配置数据卷 Docker应用部署 Docker部署mysql Docker…

Unity同时启动多个Editor

HardLinkShellExt tool https://schinagl.priv.at/nt/hardlinkshellext/linkshellextension.html 作用&#xff1a; 1.网络Online项目方便调试&#xff0c;MMO项目 2.方便发布不同平台的包&#xff0c;快速开发测试 使用方法&#xff1a;

easy-es动态索引支持

背景 很多项目目前都引入了es&#xff0c;由于es弥补了mysql存储及搜索查询的局限性&#xff0c;随着技术的不断迭代&#xff0c;原生的es客户端使用比较繁琐不直观&#xff0c;上手代价有点大&#xff0c;所以easy-es框架就面世了&#xff0c;学习成本很低&#xff0c;有空大…

【Gateway】Gateway Filter Factories

Predicate决定了请求由哪一个路由处理,如果在请求处理前后需要加一些逻辑,这就是Filter(过滤器)的作用范围了.Filter分为两种类型:Pre类型和Post类型 滤器的两种类型 Pre 类型过滤器: 执行时机: 在请求被转发到后端服务之前执行。作用: 可以用来执行鉴权、限流、请求日志记录、…

Django笔记一:搭建Django环境与URL路径访问

博主之前学从Java后端开发&#xff0c;后面获取到读研资格&#xff0c;想着未来转算法岗&#xff0c;初学Python&#xff0c;发现Python还挺有趣的&#xff0c;由于之前所学后端缘故&#xff0c;有点后端情节&#xff0c;想学习一下Django框架&#xff08;python的web框架&…

什么是交换机级联?

在现代计算机网络中&#xff0c;交换机级联是一种广泛应用的技术&#xff0c;有助于提升网络的扩展性和灵活性。本文将深入探讨交换机级联相关知识&#xff0c;详细介绍其基本概念和连接配置方法&#xff0c;并对常见技术问题进行解答。 交换机级联概述 交换机级联是指通过将…

线性基大发现

一.构造方法 1.贪心法&#xff08;每一个数往里插入即可&#xff09; /*贪心法构造线性基的特点&#xff1a; 1.从小到大排列 2.各个基的高位可能存在重复的1 2.线性基不是唯一的&#xff0c;与原集合的元素顺序有关*/ void insert(int x){//贪心法for(int i63;i>0;i--){i…

c#中给winform定义快捷键的几种方式

快捷键的使用在日常的开发中频率比较高&#xff0c;这里总结了最常见的各种快捷键的设置方式&#xff0c;需要的时候大家直接照抄就可以了&#xff0c;不用再去查询如何实现了。 文章目录 一、按钮快捷键二、菜单快捷键三、全局快捷键1、重写ProcessCmdKey2、使用KeyPreview属…