通过租户id实现的SaaS方案

Mybatisplus搭建多租户模式(共享库表,按租户id字段区分租户) 前言 近期工作中有遇到多租户模式的应用场景,对此自己查阅了大量的资料。对可行性进行分析后选择了共享库表,按租户id字段区分租户的方式去实现。以此记录一下方便日后所需查阅 1.熟悉多租户之前先来了解一下什么是SaaS系统 以下内容来着百度百科 SaaS平台是运营saas软件的平台。SaaS提供商为企业搭建信息化所需要的所有网络基础设施及软件、硬件运作平台,并负责所有前期的实施、后期的维护等一系列服务,企业无需购买软硬件、建设机房、招聘IT人员,即可通过互联网使用信息系统。SaaS 是一种软件布局模型, 阅读详情

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

d3cbba41408699655b3352eda8c4d3eb.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:juejin.cn/post
/6844903993085263886

56c05835ca400186ac8cb3868a38164b.jpeg


概况

SaaS实现的方案

  • 独立数据库

    每个企业 独立的物理数据库,隔离性好,成本高。

  • 共享数据库、独立schema

    就是一台物理机,多个逻辑数据库,oracle叫做schema,mysql叫做database,每个企业独立的schema。

  • 共享数据库、数据库表(本次采用):

    在表中添加“企业”或者“租户”字段区分是哪个企业的数据。操作的时候根据“租户”字段去查询相应的数据。

    优点:所有租户使用同一数据库,所以成本低廉。

    缺点:隔离级别低,安全性低,需要在开发时加大对安全的开发量,数据备份和恢复最困难。

改造思路
  1. 本次采用共享数据库、数据库表的SaaS方案。改造时需要做以下工作:

  • 创建租户信息表。

  • 先要将所有的表添加租户id字段tenant_id。用于关联租户信息表。

  • 将tenant_id和原始表id创建联合主键。注意主键的顺序,原表主键必须在左边。

  • 将表修改为分区表。

  1. 改造后,在添加租户信息的时候,同时在所有表中添加该租户的分区,分区用于保存该租户的数据。

  2. 在后续增加记录时,需要tenant_id字段的值,在删改查中,都需要在where条件中以tenant_id为条件来操作某个租户的数据。

测试环境介绍

测试库中有5张表,我下文使用sys_log表进行测试。

3a4f6fcb8dba82222a3466017b3e1c53.png

sys_log的建表语句为:

CREATE TABLE `sys_log` (
  `log_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `type` TINYINT(1) DEFAULT NULL COMMENT '类型',
  `content` VARCHAR(255) DEFAULT NULL COMMENT '内容',
  `create_id` BIGINT(18) DEFAULT NULL COMMENT '创建人ID',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `tenant_id` INT NOT NULL,
  PRIMARY KEY (`log_id`,`tenant_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统日志'

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

表添加租户id字段

找出未添加租户id(tenant_id)字段的表。
SELECT 
    table_name 
  FROM
    INFORMATION_SCHEMA.TABLES
  WHERE table_schema = 'my'   
    AND table_name NOT IN 
    (SELECT 
      t.table_name 
    FROM
      (SELECT 
        table_name,
        column_name 
      FROM
        information_schema.columns 
      WHERE table_name IN 
        (SELECT 
          table_name 
        FROM
          INFORMATION_SCHEMA.TABLES 
        WHERE table_schema = 'my')) t 
    WHERE t.column_name = 'tenant_id') ;

执行,找到两个符合条件的表,在数据库进行确认,确实表中没tenant_id字段。

c61c5b910101f1de2c844f5d48ccbcb6.png
创建租户信息表

仅供参考,用于保存租户信息

CREATE TABLE `t_tenant` (
  `tenant_id` varchar(40) NOT NULL DEFAULT 'c12dee54f652452b88142a0267ec74b7' COMMENT '租户id',
  `tenant_code` varchar(100) DEFAULT NULL COMMENT '租户编码',
  `name` varchar(50) DEFAULT NULL COMMENT '租户名称',
  `desc` varchar(500) DEFAULT NULL COMMENT '租户描述',
  `logo` varchar(255) DEFAULT NULL COMMENT '公司logo地址',
  `status` smallint(6) DEFAULT NULL COMMENT '状态1有效0无效',
  `create_by` varchar(100) DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `last_update_by` varchar(100) DEFAULT NULL COMMENT '最后修改人',
  `last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',
  `street_address` varchar(200) DEFAULT NULL COMMENT '街道楼号地址',
  `province` varchar(20) DEFAULT NULL COMMENT '一级行政单位,如广东省,上海市等',
  `city` varchar(20) DEFAULT NULL COMMENT '城市, 如广州市,佛山市等',
  `district` varchar(20) DEFAULT NULL COMMENT '行政区,如番禺区,天河区等',
  `link_man` varchar(50) DEFAULT NULL COMMENT '联系人',
  `link_phone` varchar(50) DEFAULT NULL COMMENT '联系电话',
  `longitude` decimal(10,6) DEFAULT NULL COMMENT '经度',
  `latitude` decimal(10,6) DEFAULT NULL COMMENT '纬度',
  `adcode` varchar(8) DEFAULT NULL COMMENT '区域编码,用于通过区域id快速匹配后展示, 如广州是440100',
  PRIMARY KEY (`tenant_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='租户的基本信息表';
将所有表添加tenant_id字段
DROP PROCEDURE IF EXISTS addColumn ;

DELIMITER ?

CREATE PROCEDURE addColumn () 
BEGIN
  -- 定义表名变量
  DECLARE s_tablename VARCHAR (100) ;
  /*显示表的数据库中的所有表
 SELECT table_name FROM information_schema.tables WHERE table_schema='databasename' Order by table_name ;
 */
  #显示所有
  DECLARE cur_table_structure CURSOR FOR 
  SELECT 
    table_name 
  FROM
    INFORMATION_SCHEMA.TABLES
  WHERE table_schema = 'my'     -- my = 我的测试数据库名称
    AND table_name NOT IN 
    (SELECT 
      t.table_name 
    FROM
      (SELECT 
        table_name,
        column_name 
      FROM
        information_schema.columns 
      WHERE table_name IN 
        (SELECT 
          table_name 
        FROM
          INFORMATION_SCHEMA.TABLES 
        WHERE table_schema = 'my')) t 
    WHERE t.column_name = 'tenant_id') ;
  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET s_tablename = NULL ;
  OPEN cur_table_structure ;
  FETCH cur_table_structure INTO s_tablename ;
  WHILE
    (s_tablename IS NOT NULL) DO SET @MyQuery = CONCAT(
      "alter table `",
      s_tablename,
      "` add COLUMN `tenant_id` INT not null COMMENT '租户id'"
    ) ;
    PREPARE msql FROM @MyQuery ;
    EXECUTE msql ;
    #USING @c; 
    FETCH cur_table_structure INTO s_tablename ;
  END WHILE ;
  CLOSE cur_table_structure ;
END ?

DELIMITER ;

#执行存储过程
CALL addColumn () ;

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

实现表分区

实现的目标:在添加租户的时候实现对所有表添加分区

需要的条件:

  • 表必须是分区表,如果不是分区表,那么需要改成分区表。

  • tenant_id必须和原表log_id主键组成联合主键。

将表修改成分区表

表中添加分区有三种方式:

  • 创建临时分区表sys_log_copy,copy数据过来后,删除旧的sys_log,再将sys_log_copy修改为sys_log(本次采用,详见下文)

  • 直接将表修改为分区表,需要原表中无数据,否则无法成功:

ALTER TABLE sys_log PARTITION BY LIST COLUMNS (tenant_id)
(
    PARTITION a1 VALUES IN (1) ENGINE = INNODB,
    PARTITION a2 VALUES IN (2) ENGINE = INNODB,
    PARTITION a3 VALUES IN (3) ENGINE = INNODB
);
  • 在分区表中添加新分区,需要表已经是分区表,否则无法成功:

ALTER TABLE sys_log_copy ADD PARTITION
(
    PARTITION a4 VALUES IN (4) ENGINE = INNODB,
    PARTITION a5 VALUES IN (5) ENGINE = INNODB,
    PARTITION a6 VALUES IN (6) ENGINE = INNODB
);
通过创建临时分区表的方式将原表转换成分区表

1. 查看表建表语句:

SHOW CREATE TABLE `sys_log`;

2、参考建表语句,创建copy表:

CREATE TABLE `sys_log_copy` (
  `log_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `type` TINYINT(1) DEFAULT NULL COMMENT '类型',
  `content` VARCHAR(255) DEFAULT NULL COMMENT '内容',
  `create_id` BIGINT(18) DEFAULT NULL COMMENT '创建人ID',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `tenant_id` INT NOT NULL,
  PRIMARY KEY (`log_id`,`tenant_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='系统日志'
PARTITION BY LIST COLUMNS (tenant_id)
(
    PARTITION a1 VALUES IN (1) ENGINE = INNODB,
    PARTITION a2 VALUES IN (2) ENGINE = INNODB,
    PARTITION a3 VALUES IN (3) ENGINE = INNODB
);

注意上文中的DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC

  • CHARSET=utf8mb4是因为utf8在mysql中是不健全的编码。

  • ROW_FORMAT=DYNAMIC是为了避免所以长度过大后导致如下报错:

ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes.

也可以在my.ini配置文件中设置为true解决这个问题,但是要重启数据库,会比较麻烦。

[mysqld]
innodb_large_prefix=true

3. 验证分区情况:

SELECT 
  partition_name part,
  partition_expression expr,
  partition_description descr,
  table_rows 
FROM
  information_schema.partitions 
WHERE TABLE_SCHEMA = SCHEMA() 
  AND TABLE_NAME = 'sys_log_copy' ;
48056f95f9c9f29434fa18f676750dab.png

4. 将数据复制到copy表中

INSERT INTO `sys_log_copy` SELECT * FROM `sys_log`

5. 删除表sys_log,再修改sys_log_copy表中的名字为sys_log。

编写自动创建分区的仓储过程

通过存储过程实现,在分区表中添加分区

DELIMITER ?

USE `my`?

DROP PROCEDURE IF EXISTS `add_table_partition`?

CREATE DEFINER=`root`@`%` PROCEDURE `add_table_partition`(IN _tenantId INT)
BEGIN
  DECLARE IS_FOUND INT DEFAULT 1 ;
  -- 用于记录游标中存在分区的表名
  DECLARE v_tablename VARCHAR (200) ;
  -- 用于缓存添加分区时候的sql
  DECLARE v_sql VARCHAR (5000) ;
  -- 分区名称定义
  DECLARE V_P_VALUE VARCHAR (100) DEFAULT CONCAT('P', REPLACE(_tenantId, '-', '')) ;
  DECLARE V_COUNT INT ;
  DECLARE V_LOONUM INT DEFAULT 0 ;
  DECLARE V_NUM INT DEFAULT 0 ;
  -- 定义游标,值是所有分区表的表名
  DECLARE curr CURSOR FOR 
  (SELECT 
    t.TABLE_NAME 
  FROM
    INFORMATION_SCHEMA.partitions t 
  WHERE TABLE_SCHEMA = SCHEMA() 
    AND t.partition_name IS NOT NULL 
  GROUP BY t.TABLE_NAME) ;
  -- 如果没影响的记录,程序也继续执行
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET IS_FOUND=0;
  -- 获取上一步中的游标中获取到的表名的个数
  SELECT 
    COUNT(1) INTO V_LOONUM 
  FROM
    (SELECT 
      t.TABLE_NAME 
    FROM
      INFORMATION_SCHEMA.partitions t 
    WHERE TABLE_SCHEMA = SCHEMA() 
      AND t.partition_name IS NOT NULL 
    GROUP BY t.TABLE_NAME) A ;
  -- 只有在存在分区表的时候才打开游标
  IF V_LOONUM > 0 
  THEN -- 打开游标
  OPEN curr ;
  -- 循环
  read_loop :
  LOOP
    -- 声明结束的时候
    IF V_NUM >= V_LOONUM 
    THEN LEAVE read_loop ;
    END IF ;
    -- 取游标的值给变量
    FETCH curr INTO v_tablename ;
    -- 依次判断分区表是否存在改分区,如果不存在则添加分区
    SET V_NUM = V_NUM + 1 ;
    SELECT 
      COUNT(1) INTO V_COUNT 
    FROM
      INFORMATION_SCHEMA.partitions t 
    WHERE LOWER(T.TABLE_NAME) = LOWER(v_tablename) 
      AND T.PARTITION_NAME = V_P_VALUE 
      AND T.TABLE_SCHEMA = SCHEMA() ;
    IF V_COUNT <= 0 
    THEN SET v_sql = CONCAT(
      '  ALTER TABLE ',
      v_tablename,
      ' ADD PARTITION (PARTITION ',
      V_P_VALUE,
      ' VALUES IN(',
      _tenantId,
      ') ENGINE = INNODB) '
    ) ;
    SET @v_sql = v_sql ;
    -- 预处理需要执行的动态SQL,其中stmt是一个变量
    PREPARE stmt FROM @v_sql ;
    -- 执行SQL语句
    EXECUTE stmt ;
    -- 释放掉预处理段
    DEALLOCATE PREPARE stmt ;
    END IF ;
    -- 结束循环
  END LOOP read_loop;
  -- 关闭游标
  CLOSE curr ;
  END IF ;
END?

DELIMITER ;

调用存储过程测试

CALL add_table_partition (8) ;
  • 如果表还不是分区表,那么调用存储过程会有如下报错:

错误代码:1505
Partition management on a not partitioned table is not possible

翻译出来的意思是:“在未分区的表上进行分区管理是不可能的”。

  • 可能会报错如下:

错误代码:1329
No data - zero rows fetched, selected, or processed

但如果通过查询下面的information_schema.partitions无误,那就是添加分区成功。

可以通过在定义游标后,打开游标之前,添加如下方式解决:

DECLARE CONTINUE HANDLER FOR NOT FOUND SET IS_FOUND=0;

SELECT 
  partition_name part,
  partition_expression expr,
  partition_description descr,
  table_rows 
FROM
  information_schema.partitions 
WHERE TABLE_SCHEMA = SCHEMA() 
  AND TABLE_NAME = 'sys_log' ;
通过mybatis调用存储过程
<select id="testProcedure" statementType="CALLABLE" useCache="false" parameterType="string">
        <![CDATA[
                call add_table_partition (
                        #{_tenantId,mode=IN,jdbcType=VARCHAR});
        ]]>
</select>

实现简单的数据权限

我们可能需要这种场景需求

  1. 集团公司有多个子公司,集团公司和每个子公司各自是一个租户,但是子公司下还有子公司。

  2. 不管是集团公司还是说旗下的子公司都有相应的用户(t_user)。

  3. 用户需要有查看自己公司下的数据和下面子公司的数据的权限。

从上面的场景需求中,我们知道,t_tenant表需要设计成树桩结构。下面我们进行测试。

修改上文的t_tenant表为:
CREATE TABLE `t_tenant` (
  `tenant_id` VARCHAR(40) NOT NULL DEFAULT '0' COMMENT '租户id',
  `path` VARCHAR(200) DEFAULT NOT NULL COMMENT '从根节点开始的id路径树,如:0-2-21-211-2111,通过"-"隔开,最末尾为自己id',
  `tenant_code` VARCHAR(100) DEFAULT NULL COMMENT '租户编码',
  `name` VARCHAR(50) DEFAULT NULL COMMENT '租户名称',
  `logo` VARCHAR(255) DEFAULT NULL COMMENT '公司logo地址',
  `status` SMALLINT(6) DEFAULT NULL COMMENT '状态1有效0无效',
  `create_by` VARCHAR(100) DEFAULT NULL COMMENT '创建者',
  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
  `last_update_by` VARCHAR(100) DEFAULT NULL COMMENT '最后修改人',
  `last_update_time` DATETIME DEFAULT NULL COMMENT '最后修改时间',
  `street_address` VARCHAR(200) DEFAULT NULL COMMENT '街道楼号地址',
  PRIMARY KEY (`tenant_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='租户的基本信息表'
  • 为了演示,删除了些感觉没是没用的字段

  • 添加了path字段,实现租户和子租户的树形结构

添加测试数据

新增租户信息:

通过path缓存着t_tenant树的路径。

73c035935a017eea4dbfdaa89e543174.png

创建用户表(t_user),添加测试用户:

测试的用户id和tenant_id需要对应

b54de2b90795ea01c48b562ddb4b69cd.png

创建附件表(t_file),添加测试业务数据:

创建人字段(create_by)关联用户表(t_user),也要关联到租户(tenant_id),指明是哪个子公司的数据。

f208ea510ad3494c846b7217b867540e.png
进行测试
  • 查看tenant_id是"211"的租户信息和其下的子租户信息

    SELECT 
      tt.`tenant_id`,
      tt.path 
    FROM
      t_tenant tt 
    WHERE 
      (SELECT 
        INSTR(tt.path, "211")) ;
  • 查看tenant_id是"211"的附件和其下的子租户的附件信息

    78360ce863170c686b776c347fc8a005.png
    SELECT 
      * 
    FROM
      t_file tf 
    WHERE tf.`tenant_id` IN 
      (SELECT 
        tt.`tenant_id` 
      FROM
        t_tenant tt 
      WHERE 
        (SELECT 
          INSTR(tt.path, "211"))) ;
  • 查看tenant_id是"2"的附件和其下的子租户的附件信息

    66c9652d8c341774d4f2ca40ff9e4898.png
通过mybatis拦截器实现查看子租户的数据权限

编写拦截器:

package com.iee.orm.mybatis.common;

import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.iee.orm.mybatis.common.UserHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.context.annotation.Configuration;

import java.sql.Connection;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


 * 实现 拦截select语句,实现尾部拼接sql来查询本租户和子租户信息
         * @author longxiaonan@aliyun.com
 */
@Slf4j
@Configuration
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlInterceptor implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

    MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedstatement");
    if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
            || StatementType.CALLABLE == mappedStatement.getStatementType()) {
      return invocation.proceed();
    }

    getSqlByInvocation(metaObject, invocation);
    return invocation.proceed();
  }


     * 拼接sql执行
     * @param metaObject
     * @param invocation
     * @return
             */
  private String getSqlByInvocation(MetaObject metaObject, Invocation invocation) throws NoSuchFieldException, IllegalAccessException {

    String originalSql = (String) metaObject.getValue(PluginUtils.DELEGATE_BOUNDSQL_SQL);
    metaObject.setValue(PluginUtils.DELEGATE_BOUNDSQL_SQL, originalSql);
    String targetSql = addDataSql(originalSql);
    return targetSql;

  }


     * 将原始sql进行拼接
     * @param sql
     * @return
             */
  static String addDataSql(String sql) {

    sql = StringUtils.replace(sql, ";", "");
    StringBuilder sb = new StringBuilder(sql);
    String tenantId = UserHelper.getTenantId();


    String suffSql = " `tenant_id` IN " +
            "(SELECT " +
            "tt.`tenant_id` " +
            "FROM " +
            "t_tenant tt " +
            "WHERE " +
            "(SELECT " +
            "INSTR(tt.path," + tenantId + "))) ";

    String regex = "(.*)(where)(.*)";
    Pattern compile = Pattern.compile(regex);
    Matcher matcher = compile.matcher(sql);
    if (matcher.find()) {
      String whereLastSql = matcher.group(matcher.groupCount());

      int left = StringUtils.countMatches(whereLastSql, "(");
      int right = StringUtils.countMatches(whereLastSql, ")");
      if(left == right){
        sb.append(" and ");
        sb.append(suffSql);
        log.info("数据权限替换后sql:--->" + sb.toString());
        return sb.toString();
      }
    }

    sb.append(" where ");
    sb.append(suffSql);
    log.info("数据权限替换后sql:--->" + sb.toString());
    return sb.toString();
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, new SqlInterceptor());
  }

  @Override
  public void setProperties(Properties properties) {
  }
}

用到了mybatis-plus的工具类

/*
 * Copyright (c) 2011-2020, baomidou (jobob@qq.com).
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * <p>
 * https://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.baomidou.mybatisplus.core.toolkit;

import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.lang.reflect.Proxy;
import java.util.Properties;

/**
 * 插件工具类
 *
 * @author TaoYu , hubin
 * @since 2017-06-20
 */
public final class PluginUtils {
  public static final String DELEGATE_BOUNDSQL_SQL = "delegate.boundSql.sql";

  private PluginUtils() {
    // to do nothing
  }

  /**
   * 获得真正的处理对象,可能多层代理.
   */
  @SuppressWarnings("unchecked")
  public static <T> T realTarget(Object target) {
    if (Proxy.isProxyClass(target.getClass())) {
      MetaObject metaObject = SystemMetaObject.forObject(target);
      return realTarget(metaObject.getValue("h.target"));
    }
    return (T) target;
  }

  /**
   * 根据 key 获取 Properties 的值
   */
  public static String getProperty(Properties properties, String key) {
    String value = properties.getProperty(key);
    return StringUtils.isEmpty(value) ? null : value;
  }
}

测试代码见:

https://github.com/longxiaonan/java-sea/tree/master/javasea-orm/javasea-orm-mybatis-Interceptor


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,“长按”或“扫描”下方二维码噢:

91065c13700aa1f203c5018bf33bf6f5.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

77a881c02992ad0a6a02e5ab6c14c496.png

1ef7a36b36cfd224bd0333b8ef4beb0d.pngd1a3e198c203f6729540b3b9f4a9aa84.pngd32762dac984f0e2345dea279b9a9a0e.png993e6cd9de2b8290a6b648e55d64e512.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
微服务系统SAAS化方案思考(基于现状少改动)(未实施) 方案1:共享数据库,共享数据架构 方案2:共享数据库,独立数据架构 方案3:独立数据库 根据系统现状和业务规划选择合适的方案,如果不同租户有共享也有独立数据库的,可以考虑根据租户id配置,把租户id和redis、数据库配置在租户资源表中,请求可以根据资源表路由到不同资源库中。我们是针对现有系统saas改造,要减少代码调整,数据库使用公司客户端组件,如果希望sql无改造可能较复杂或解析拼接租户id影响性能,我们系统选择共享数据共享数据架构的方式,故最终的改造方案: 1.rpc接口层增加租户注解,把租户 阅读详情

相关推荐

使用 Java + PaddleOCR 实现表格识别,支持导出 HTML 和 Excel(附源码)

使用Java+PaddleOCR快速搭建表格识别系统,无需处理底层模型细节。系统支持表格结构识别和内容识别,可将图片转换为可编辑表格格式。文章提供了完整的实现流程,包括Maven依赖引入、模型下载(包含检测、识别、分类和表格结构模型)及参数配置说明。SmartJavaAI支持多种高性能模型选择,适用于不同应用场景,帮助开发者快速实现表格识别功能。

deng775747758的博客 1476

Mybatis Plus 多租户id使用

本文就不多逼逼,直接进入正题。多租户技术(Multi-TenancyTechnology)又称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下 (此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲: 在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架 构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重 点就是同一套程序下实现多用户数据的隔离目

BASK2311的博客 6558

深入MIPI DSI显示技术栈 (三) DSI vs CSI-2:孪生协议的本质差异

MIPI协议中的DSI与CSI-2存在本质差异:DSI为输出导向协议(Host→Display),用于屏幕控制和画面传输;CSI-2为输入导向协议(Sensor→Host),专攻图像采集。二者在包结构、同步机制、物理层优化等方面均有显著区别:DSI支持命令包和BLLP包,CSI-2具备嵌入式数据包;DSI时钟模式以Burst为主,CSI-2支持Continuous模式。典型场景中,二者协同工作(如手机相机预览),需注意时序同步和延迟控制。设计混合系统时,需合理规划虚拟通道和物理层资源,充分发挥这对&quot

GateWorld的博客 2110

实战saas系统多租户数据隔离(二)使用租户id字段区分租户数据

目录 0.前言 1.whymybatis-plus? 2.需求分析 3.环境准备 4.实现步骤 4.1准备TenantContext 4.2创建MybatisPlusConfig配置类 4.3编写接口 4.4编写拦截器,在接口调用前进行多租户处理 5.项目测试 总结 参考资料 0.前言 上一篇文章中,我们一起了解了一下saas系统架构中实现...

johntsu的专栏 1万+

CanOpen PDO映射实战:从原理到配置的完整指南

本文详细解析了CanOpen PDO映射的原理与配置流程,从基础通信机制到实战配置步骤,涵盖禁用PDO、清空映射、逐项映射和使能PDO等关键环节。特别针对西门子G120变频器、台达伺服和倍福CX控制器等不同厂商设备,提供了具体的配置技巧和常见问题排查指南,帮助工程师快速掌握多厂商设备间的PDO通信配置。

weixin_30718391的博客 517

从量子兔AlphaPi硬件拆解到ESP32-C3库开发实战

本文详细拆解了量子兔AlphaPi开发板的硬件构成,重点分析了其核心ESP32-C3芯片及外围传感器。通过逆向工程揭示了GPIO引脚定义与通信协议,并实战演示了如何从零开发MicroPython库,驱动按键、加速度计及LED点阵,最终整合成一个互动平衡球游戏项目,展现了开源硬件在教育与二次开发中的潜力。

weixin_29164497的博客 460

【建议收藏】通过租户id实现的SaaS方案

概况项目开发到一半,用户突然提出需要多个分公司共同使用,这种需要将系统设计成SaaS架构,将各个分公司的数据进行隔离。SaaS实现的方案独立数据库每个企业 独立的物理数据库,隔离性好,成本高。共享数据库、独立schema就是一台物理机,多个逻辑数据库,oracle叫做schema,mysql叫做database,每个企业独立的schema。共享数据库、数据库表(本次采用):在表中添加“企业”或者“...

独行侠梦的博客 862

Ollama系列---【ollama使用gpu运行大模型】

一、安装CudaToolkit 使用控制台命令查看当前显卡驱动中的cuda版本 nvidia-smi 二、下载CudaToolkit 注意:CudaToolkit版本要低于上面的显卡Cuda版本。 Cuda各版本下载地址:https://developer.nvidia.com/cuda-toolkit-archive 下载好之后,一路下一步,一直默认即可。 如果安装到了自定义的位置,记得加环...

-少年-的博客 7988

使用租户ID,实现一个企业级的SaaS系统,完整方案来了!

????这是一个或许对你有用的社群????一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:《项目实战(视频)》:从书中学,往事中“练”《互联网高频面试题》:面朝简历学习,春暖花开《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题《精进 Java 学习指南》:系统学习,互联网主流技术栈《必读 Java 源码专栏》:知其然,知其所以然????这是一个或许...

weixin_44421461的博客 694

【OpenFOAM】-olaFlow-算例6- waveFloatingObject

波浪作用下的浮体的刚体运动,属于流固耦合(FSI)问题

ou_no的博客 3541

一个中科大差生的8年程序员工作总结

今年终于从大菊花厂离职了,离职前收入大概60w不到吧,在某乎属于比较差的,今天终于有空写一下自己的职场故事,也算是给自己近8年的程序员工作做个总结复盘。近8年有些事情做对了,也有更多事情做错了,在这里记录一下,希望能够给后人一些帮助吧,也欢迎私信交流。文笔不好,见谅,有些细节记不清了,如果有出入,就当是我编的这个故事吧。 PS:有几个问题先在这里解释一下,评论就不一一回复了 1、关于差生,我本人在科大时确实成绩偏下,差生主要讲这一点,没其他意思。 2、因为买房是我人生中的大事,我认为需要记录和总结一下

陈小房的自我修炼 5万+

通过租户 id 实现的 SaaS 方案

来源:juejin.cn/post/6844903993085263886???? 欢迎加入小哈的星球,你将获得:专属的项目实战 / Java 学习路线 /一对一提问 / 学习打卡 / 赠书福利全栈前后端分离博客项目 1.0 版本完结啦,2.0 正在更新中...,演示链接:http://116.62.199.48/,全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,...

犬小哈 260

Google antigravity 不支持你所在地区?手把手教你把账号改成支持地区

Google antigravity功能火爆,但国内老账号根本用不了🤬本文分享几种有效的账号国家地区切换方法,最快几分钟就能让antigravity直接亮起🔥

XiaoqiangClub的博客 2万+

腾讯面试:SaaS多租户,如何设计?

多租户是SaaS(Software-as-a-Service)下的一个概念,意思为软件即服务,即通过网络提供软件服务。SaaS平台供应商将应用软件统一部署在自己的服务器上,客户可以根据工作的实际需求,通过互联网向厂商租用所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得SaaS平台供应商提供的服务。

架构师尼恩 1701

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

VVVVVV 社区运营完全指南:从 Discord 频道切分到 Issue 标签,9 个可复制的运营动作

VVVVVV 是一款运营多年的开源平台解谜游戏,它的社区运营经验是中小开源项目难得的样本。维护代码只是一半工作,另一半是让玩家的反馈一直流动——多数项目恰恰败在这条线上。这篇文章把 VVVVVV 在 Discord 与 GitHub 两侧的做法拆成可照抄的动作,讲清社区运营与社区管理的落地路径。 ## 🕳️ 讨论区为什么沉寂:三个杀死社区的典型场景 先说三个你大概率见过的场面。 - 玩家在

gitblog_00785的博客 639

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

SaaS多租户架构实践:字段隔离方案(共享数据库+共享Schema)

本项目演示了SaaS模式下基于字段隔离的多租户架构实现方案。采用MyBatis-Plus的TenantLineInnerInterceptor拦截器,通过自定义MyTenantLineHandler实现租户ID自动注入SQL。核心组件包括:1)租户上下文(TenantContext)使用ThreadLocal存储租户ID;2)过滤器(TenantFilter)从请求头提取租户ID;3)MyBatis配置类集成多租户拦截器。实现了共享数据库+共享Schema场景下的数据隔离,仅作技术验证使用。

wangzhilong1996的博客 949

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

实战saas系统多租户数据隔离 文章汇总

把这几天写的文章做一个汇总,方便大家查找 saas系统多租户数据隔离的实现(一)数据隔离方案 saas系统多租户数据隔离的实现(二)使用租户id字段区分租户数据 saas系统多租户数据隔离的实现(三)每个租户使用独立的表空间 ...

johntsu的专栏 2649

SaaS私有部署多租户方案与考量

SaaS 多租户 多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。 三种数据隔离方案 1. 独立数据库 即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。 优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。 缺点:增多了数据库的安装数量,随之带来维护成本和购置成本的增加。 2. 共享数据

weixin_44371237的博客 1447

SaaS-轻租户设计

项目场景: SaaS是现在市场上比较火的一种模式,其实就是为了中小软件商服务,所以我们一套系统可能会有多个客户去使用那么这样就会出现一个多租户的一个概念。 问题描述: 同一个服务下的租户设计: 1.轻租户-数据隔离-使用同一个数据库同一张表,用字段区别隔离 2.重租户-数据隔离-使用不同的数据库不同的表去做数据隔离 如果是基于轻租户的设计模式,那么就是在设计表的时候,加上一个租户的字段作为数据隔离 例如我的user表为 CREATE TABLE `user` ( `id` varchar(32)

weixin_43860337的博客 653

SAAS系统中,多租户如何实现?

这个方案是最常见的,因为他足够简单,只需要加个字段就行了。但是缺点是他只是逻辑隔离,存在数据泄露的风险,并且多个租户共用一个数据库,可能会存在互相影响的情况。比如根据用户规模或需求动态选择隔离级别,针对小型用户,大家共用一个公共数据库,对于一些大客户,VIP客户,给他独立的分区,对于一些数据安全性要求比较高,受监管,愿意给更多钱的用户,给他独立部署。在SaaS系统中,多租户是个基本功能了,就是你服务的每一个公司有一个自己的租户,系统要同时支持多个租户一起使用,并且他们之间要互相隔离。那么,好一点的方案是。

qq_38196449的博客 502

多租户 SaaS 前端实现完整指南:yudao-ui-admin-vben 如何用 tenant-id 与 visit-tenant-id 实现一键切换租户

yudao-ui-admin-vben 是芋道管理后台基于 Vue3、Vite6、Ant Design Vue 与 TypeScript 重构的多租户 SaaS 前端,本文带你完整看懂它的租户机制核心——`tenant-id`(登录租户)与 `visit-tenant-id`(访问租户)双请求头:如何做到数据隔离,又如何让平台管理员一键"穿越"到任意租户查看数据。 ## 🔑 一句话看懂多租户机

gitblog_00144的博客 807

MyBatis多租户隔离插件开发

在SASS的大潮流下,相信依然存在很多使用一个数据库为多个租户提供服务的场景,这个情况下一般是多个租户共用同一套表通过sql语句级别来隔离不同租户的资源,比如设置一个租户标识字段,每次查询的时候在后面附加一个筛选条件:TenantId=xxx。这样能低代价、简单地实现多租户服务,但是每次执行sql的时候需要附加字段隔离,否则会出现数据错乱。 此隔离过程应该自动标识完成,所以我今天借助于My...

u014352080的专栏 1669

SaaS多租户系统改造

创建sys_tenant表,用于存储租户的基本信息在所有需要隔离数据的表中添加tenant_id字段,用于标识记录属于哪个租户创建一个租户上下文工具类,使用ThreadLocal存储和获取当前请求的tenant_id自定义拦截器来获取请求中的租户标识,并存储在上下文中,权限验证时,添加租户验证逻辑在MybatisPlusConfig中配置TenantLineInnerInterceptor,实现自动拼接SQL的tenant_id条件。

weixin_45336090的博客 232

SAAS系统架构之多租户设计

一、概述 采用“共享数据库,共享数据架构方案”,重点在于租户管理和数据隔离,租户管理:包括注册、订购、计费等,数据隔离:变更表结构,增加tenant_id字段。 二、租户管理 1.注册 2.订购 3.计费 三、数据隔离 ...

深漂小码哥 2306

SaaS多租户数据隔离实战:MyBatis拦截器实现行级安全方案

本文详细讲解如何在SaaS系统中使用MyBatis拦截器实现多租户数据隔离,包含完整的代码实现、流程图和最佳实践。

Java领域创作者 1376
上一篇: 推荐一个灵活可配置的开源监控平台,功能非常强大!
下一篇: SpringBoot 动态权限校验,优雅的实现方案
公众号-芋道源码
博客等级 码龄10年 1636粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值