基于租户 ID 的 SaaS 解决方案

小夏 科技 更新 2024-02-01

概述

在项目开发到一半时,用户突然提出需要多个分支一起使用,这需要将系统设计成SaaS架构,将各个分支的数据隔离开来。

SaaS 实施

独立数据库:每个企业独立物理数据库,隔离性好,成本高。 共享数据库和独立架构是一台物理机和多个逻辑数据库,分别称为 oracle 的 schema 和 mysql 的数据库,每个企业都有一个独立的 schema。 共享数据库和数据库表(本次使用):在表中添加“企业”或“租户”字段,以区分数据是哪个企业。 在操作过程中,您可以根据租户字段查询相应的数据。 优点:所有租户使用相同的数据库,因此成本较低。 缺点:隔离级别低,安全性低,开发过程中需要增加安全开发量,数据备份和恢复难度最大。 转型思路

此次采用共享数据库和数据库表的SaaS方案。 在转换过程中,需要执行以下操作: 创建租户信息表。 首先,将租户 ID 字段添加到所有表中。 用于关联租户信息表。 为租户 ID 和原始表 ID 创建联合主键。 注意主键的顺序,原表的主键必须在左边。 将该表修改为分区表。 转换后,在添加租户信息时,还可以将租户的分区添加到所有表中,以存储租户的数据。 添加记录时,需要使用租户 ID 字段作为租户 ID 字段的值,并使用租户 ID 作为条件,以便在删除和修改查询时操作 where 条件中的租户数据。 测试环境介绍

测试库中有 5 个表,下面我将使用 sys 日志表进行测试。

为 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='系统日志'
将租户 ID 字段添加到表中

查找未添加“租户 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') ;
执行命令,找到两个满足要求的表,在数据库中检查该表中没有租户 ID 字段。

创建租户信息表

仅供参考,以保存租户信息。

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 '公司徽标地址', `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='租户基本信息表';
将租户 ID 字段添加到所有表

drop procedure if exists addcolumn ;delimiter ?创建过程 addcolumn ()begin --define table name 变量 declare s tablename varchar (100) ;* 数据库中显示该表的所有表,从信息模式中选择表名tables where table_schema='databasename' order by table_name ;显示从信息架构中选择表名的所有声明 cur 表结构游标tables where table_schema = 'my'--my = 我的测试数据库名称和表名不在 (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 ;执行存储过程调用 addcolumn()。
实现表分区

目标:在添加租户时向所有表添加分区。

所需条件:

该表必须是分区表,如果不是分区表,则需要将其更改为分区表。 租户 ID 必须与原始表的日志 ID 的主键组合。 将表修改为组件表

有三种方法可以向表添加分区:

创建一个临时分区表sys日志副本,删除旧的sys日志,然后将sys日志副本修改为sys日志(这次详见下文)直接将表修改为分区表,不需要原表中的数据,否则不会成功

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);
您可以创建临时分区表,将原表转换为分区表。

查看创建表的语句

show create table `sys_log`;
2. 参考建表语句创建副本表

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);
注意上面默认的 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.

也可在我的ini配置文件设置为true可以解决这个问题,但是重启数据库会很麻烦。

mysqld]

innodb_large_prefix=true

验证分区:
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' ;
您可以查看已添加的三个分区

将数据复制到复制表

insert into `sys_log_copy` select * from `sys_log`
5. 删除 sys 日志表,然后将 sys 日志复制表中的名称更改为 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 ;声明为 v tablename varchar (200) 的表名,用于记录游标中是否存在分区将分区添加到缓存时 SQL 声明 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 ;定义一个游标,其值为所有分区表的表名声明 curr 游标 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) ;如果没有受影响的记录,程序将继续执行声明继续处理程序,以便找到未找到集 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 ;仅当 v loonum > 0 时才打开光标,然后 --open curr ;循环读取循环 : 循环 -- 声明循环的结束 if v num >= v loonum then le**e read loop ; end if ;将变量 fetch curr 的游标值取到 v tablename 中;如果没有,则添加一个分区集 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;释放预处理段解除分配准备stmt; end if ;结束循环读循环;- 关闭 curr ; end if ;end?delimiter ;
调用存储过程测试。

call add_table_partition (8) ;
如果表未分区,则在调用存储过程时会报告以下错误:错误**:无法对未分区表进行 1505partition 管理,即“无法对未分区表进行分区管理”。 可能会报告以下错误:错误**:1329no data - 提取、选择或处理的行数为零,但如果查询以下信息架构分区正确,即分区添加成功。 这可以通过在定义游标之后和打开游标之前添加以下内容来解决:
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 调用的。

实现简单的数据权限

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

一个集团公司有多个子公司,每个集团和每个子公司本身就是一个租户,但子公司下也有子公司。 无论是集团公司还是其子公司,都有相应的用户(t users)。 用户需要有权限才能在下面查看自己公司的数据和子公司的数据。 从上面的场景需求中,我们知道 t 租户表需要设计为树桩结构。 让我们来测试一下。

将上面的 T 租户表修改为:

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 '公司徽标地址', `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='租户基本信息表'
修改地点是:
为了便于演示,我们删除了一些感觉并非无用的字段,并添加了路径字段来实现租户和子租户的树结构添加测试数据新增租户信息:

T 租户树的路径通过路径进行缓存。

创建用户表(t 用户)并添加测试用户

测试的用户 ID 和租户 ID 必须对应。

创建附件表(t文件)并添加测试业务数据

“创建者”字段与“T 用户”表相关联,也与租户 ID 相关联,指示数据是哪个子公司。

参加考试

检查租户 ID 是什么"211"的租户信息及其下的子租户信息。

select

tt.`tenant_id`,tt.path

fromt_tenant tt

where

select

instr(tt.path, "211"检查租户 ID 是什么"211"以及 SELECT 下子租户的附件信息

fromt_file tf

where tf.`tenant_id` in

select

tt.`tenant_id`

fromt_tenant tt

where

select

instr(tt.path, "211")))

检查租户 ID 是什么"2"以及其下子租户的附件信息你可以使用 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 j**a.sql.connection;import j**a.util.properties;import j**a.util.regex.matcher;import j**a.util.regex.pattern;* 实现截取SELECT语句,实现尾部串联SQL查询租户和子租户的信息 * 作者longxiaonan@aliyuncom */@slf4j@configuration@intercepts()}public class sqlinterceptor implements interceptor getsqlbyinvocation(metaobject, invocation); return invocation.proceed();拼接 SQL 执行 * 参数元对象 * 参数调用 * 返回 * 私有字符串 getsqlbyinvocation(metaobject metaobject, invocation invocation) 抛出 nosuchfieldException, illegalaccessexception * 连接原始 sql * 参数 sql * return * 静态字符串 adddatasql(string sql) sbappend(" where "); sb.append(suffsql); log.info("sql:--替换数据权限后" + sb.tostring())return sb.tostring();override public object plugin(object target) override public void setproperties(properties properties) }
使用了 mybatis-plus 实用程序类。

/* *copyright (c) 2011-2020, baomidou ([email protected]). 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 * 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 j**a.lang.reflect.proxy;import j**a.util.properties;** 插件实用程序类 * 作者 陶宇、湖滨 * 自 2017-06-20 起 * 公共最终类 Pluginutils ** 获取真实的处理对象,可能是多层的**。 / @suppresswarnings("unchecked") public static t realtarget(object target) return (t) target;根据 key * public static string getproperty(properties properties, string key) } 获取属性的值
在测试过程中发现,只要使用 select 语句,就会关联查询子租户的信息。 
测试 ** 请参阅:

·end·

作者:萧岚子**:觉进CN POST 6844903993085263886版权声明:内容仅供学习研究之用,版权归原作者所有。 如有任何侵权行为,请告知我们,我们将立即删除并道歉。 谢谢!

相似文章

    实现从CAN到485的高效转换方法

    在工业自动化领域,CAN和RS 是广泛使用的通信协议。但是,有时我们需要将CAN信号转换为信号,例如在远程监控或数据传输中。那么,如何实现这种高效的转换呢?本文将为您揭开这个过程的神秘面纱。.了解协议的特点。首先,我们需要深入了解CAN和RS 之间的功能和区别。CAN是一种具有高速 高效率 抗干扰能...

    通过社区团购小程序,实现邻里之间的互动交流

    随着科技的飞速发展,数字生活已经成为我们日常生活中不可或缺的一部分。在这个过程中,移动应用 小程序等新兴技术在社区服务中发挥着越来越重要的作用。其中,社区 小程序作为一种新型的社区服务工具,不仅为居民提供了便捷的购物体验,也成为促进邻里互动交流的重要平台。.社区 小程序的兴起与发展。社区 小程序是近...

    全局代理IP如何工作以及如何实现

    前言。在网络中,服务器是获取网络资源的一种方式。全局 IP 的工作原理是将所有网络请求重定向到服务器,该服务器完成对目标的访问并传递数据。.全球 IP的工作原理。在实现全局IP的过程中,我们需要使用软件将原来的网络请求重定向到服务器,然后服务器将完成数据传输。在这个过程中,我们需要使用两个关键概念,...

    在道路上和时代中实现自信自强的根本方向(1)。

    题目 时代自信与自强 我们的根本方向。文 在这个时代,我们面临着无数的挑战和机遇。我们的国家 我们的社会和我们的人民都在不断发展和进步。然而,这一切的背后,我们离不开我们对自己道路的自信和对时代的自强不息。这是我们的基本方向,也是我们的核心价值观。首先,我们需要对道路充满信心。道路自信,是我们选择的...

    犹太人的智慧是实现财务自由的方式和手段

    在生活中,金钱似乎无处不在,无论是个人 企业 政治组织等,都有赚取财富和实现财富的能力财务自由愿望。在创造财富的领域,犹太人似乎总是能够脱颖而出。这让人不禁要问 为犹太人赚钱的想法到底是什么?他们如何成功赚钱,如何管理财富以获得更多?有些人试图用种族 信仰等标签来回答这个问题,但毫无疑问,单一因素绝...