如何实现多租户系统

news2024/11/28 13:56:25

shigen日更文章的博客写手,擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长,分享认知,留住感动。

不知道为什么,最近老是有一些失眠,凌晨睡,两点半还在醒着。脑子里想着自己生活、vlog计划……就是怎么睡不着。实在是没啥可干的了,我拿起了电脑,写着博客,反正迟早是要写的。

加班写博客中

今天要分享的主题是《mybatis实现租户拦截器》。具体的需求是这样的,shigen在周五的时候接收到了这样的一个任务:实现系统的多租户。一想,系统的用户不到10个,还多租户。不抱怨,不抄旧的代码,我开始了研究。

相信大家看到的教程也主要是两种方式实现多租户。

多租户的实现方式

共享数据库、数据表

这种方式我们直接共享数据库和数据表,在每个数据表中加上tenant_id字段做数据的隔离,类似于这样的:

select * from user where tenant_id = '100001';

那这种方式的优点肯定是显而易见的,简单到家了,实现起来也是快速的(相对第二种方式而言),运维的成本也很小。

但是缺点更加的显而易见:

  1. 数据的隔离性差。让我想到了哈希环的数据倾斜。一个租户下边的数据很多,另一个租户下边的数据很少,势必会影响性能;
  2. 每个表都要加字段,很大的侵入性;
  3. 数据备份难。实现基于单租户的数据备份显得很难了。
实现

那实现上估计又有人皱起了眉头,因为这样的话,项目之前的代码都要改,每个查询的语句都要加上enant_id =xxx 的代码。像shigen这样讨厌重复性工作的人,这得加到猴年马月,而且很容易仍人心烦意乱,emo,哭爹骂娘……

那这种更好的方式就是在sql执行之前做一个统一的拦截,拼上租户的条件。别急,今天的代码案例shigen就会降到这种方式的具体实现。

我必须承认这不是一种很好的方式,也是我一直在思考的TB业务和TC业务的区别。万一用户的数据量急剧的增上,就像某个大集团,动辄好几十万员工,沉淀的各种业务数据是很可怕的。还用一个数据库,玩什么分布式、高并发。

共享数据库,独立一张表

这里只是讲一下概念哈。这里我们获得当前用户的tenant_id,然后再读取和写入查表的时候,在表名字后边拼接上tenant_id

如:张三的租户ID=‘10001’,所以他的数据存放在user_10001

相较于第一种方式,这种方式的优点在于数据的隔离性更好,数据的侵入性小。

但是缺点也依旧存在,操作租户产生的效率问题依旧的存在,备份依旧的困难。

所以,更好的方式出现额。

独立数据库

这个是有落地实现的案例的。shigen的文章spring boot+mybatis实现读写分离中有异曲同工之妙,实现了多数据源的切换,这里的实现也是类似的,一个租户一个数据库,数据库的数据表都是相同的。我们在查询的时候,就根据租户ID进行动态的路由数据源。

这样实现下来岂不是很nice。拓展性、隔离性都是绝对的nice。

但是缺点也是有的,维护的成本高了吧(当然,数据量不大的情况下,忽略不计)。


说了半天的理论,我们来实践一下吧。shigen还是采用的第一种方式。

实话说,shigen在自己写着代码之前,也找了很多的教程,但都存在着代码质量不高、功能不能实现、考虑问题不全面等问题。

代码实现

自定义注解

这里的作用是标注一下哪些数据的查询是需要用tenant_id进行过滤的。毕竟系统字典、公共数据我们还是得老老实实的放行。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Tenant {

    boolean flag() default true;
}

注意作用域,只能坐拥在方法上,怀着为什么的心态继续往下看。

userMapper.xml

选取了在mapper层的两个接口方法:

    @Tenant
    User selectByPrimaryKey(Long id);

    @Tenant
    List<User> selectSelective(User user);

相同点是都用了注解且都是根据条件查询的。

xml不展示了,都是魔法生成的,注意,没有tenant_id的存在!它只存在user表中。

拦截器

重头戏来了,拦截器可是核心!

@Component
@Slf4j
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})

public class TenantInterceptor implements Interceptor {

    public static final String TENANT_ID = "tenant_id";
    public static final String WHERE = "where";
    public static final String FROM = "from";
    public static final String FAKE_TENANT_ID = "'string'";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // id为执行的mapper方法的全路径名,如com.gitee.shigen.mapper.UserMapper.selectByPrimaryKey
        String id = mappedStatement.getId();
        // sql语句类型 select、delete、insert、update
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        BoundSql boundSql = statementHandler.getBoundSql();

        // 获取到原始sql语句 带?号
        String sql = boundSql.getSql().toLowerCase();

        // 注解逻辑判断  添加注解了才拦截
        // 如:com.gitee.shigen.mapper.UserMapper
        Class<?> classType = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf(".")));
        // selectByPrimaryKey
        String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1);
        for (Method method : classType.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Tenant.class) && mName.equals(method.getName())) {
                Tenant tenantId = method.getAnnotation(Tenant.class);
                if (tenantId.flag() && sqlCommandType.equals(SqlCommandType.SELECT)) {
                    StringBuilder sb = new StringBuilder(sql);
                    if (sql.contains(WHERE)) {
                        int whereIndexOf = sql.indexOf(WHERE);
                        sb.insert(whereIndexOf + WHERE.length(), " " + TENANT_ID + "=" + FAKE_TENANT_ID + " and ");
                    } else {
                        // 不存在where
                        sb.insert(getTableNameAfterIndex(sql) + 1, WHERE + " " + TENANT_ID + "=" + FAKE_TENANT_ID + " ");
                    }
                    sql = sb.toString();
                }
            }
        }

        // 通过反射修改sql语句
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, sql);
        return invocation.proceed();
    }


    private int getTableNameAfterIndex(String sql) {
        int fromIndex = sql.indexOf(FROM);
        return sql.indexOf(" ", fromIndex + 5);
    }
}

区区代码,折腾了俩天,但是想起来是值得的。

主要的原理就是这样的,我们正常的查询是这样的:

select * from user where id =1;
select * from user;

对于存在where关键字的,我直接在where后边拼上tenant_id的条件;

不存在的,那更好办了,直接where tenan_id =xxx

一想,你这样合适吗?

SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS date, COUNT(*) AS count
FROM user
GROUP BY create_time
ORDER BY create_time DESC;

这样的代码直接G了。直接拼上去,sql的语法都检查不过,就别提数据的隔离了。

为此,我还写了一个接口方法验证呢。

    @Tenant
    List<UserCountByCreateTimeVo> getUserCount();
  <select id="getUserCount" resultType="com.gitee.shigen.vo.UserCountByCreateTimeVo"
      resultMap="UserCountByCreateTimeVo">
      SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS date, COUNT(*) AS count
      FROM user
      GROUP BY create_time
      ORDER BY create_time DESC;
  </select>

不多说了,我说一下我的优化点,也相当于是我作为创作者制定的一个规范:

  • sql拦截器只处理带了注解的方法,且方法的sql是查询的;
  • 不存在where关键字,我就通过from关键字定位。

好了,到此,大部分的工作已经结束了,我们可以松一口气了。

接口测试
@RestController
@RequestMapping(value = "user")
public class UserController {

    @Resource
    private UserMapper userMapper;

    @GetMapping(value = "{id}")
    public User getById(@PathVariable("id") Long id) {
        return userMapper.selectByPrimaryKey(id);
    }

    @GetMapping
    public List<User> getUser(User user) {
        return userMapper.selectSelective(user);
    }

    @GetMapping(value = "userCount")
    public List<UserCountByCreateTimeVo> getUserCount() {
        return userMapper.getUserCount();
    }

}

比较粗狂,但是都是为了测试,这里就先这样啦。

测试

我们来跑起项目测试一下:

根据ID查询

根据ID查询

sql是这样的:

select id,username,password, nickname,phone,introduction, avatar,create_time,update_time, is_deleted,tenant_id from user where tenant_id='string' and id = ?
模糊查询

模糊查询

sql是这样的:

select * from user where tenant_id='string' and username like concat('%', ?, '%') and password = ? order by create_time desc
不给条件查询

不给条件查询

sql是这样的:

select * from user where tenant_id='string' order by create_time desc
聚合查询

聚合查询

sql是这样的:

select date_format(create_time, '%y-%m-%d') as date, count(*) as count from user where tenant_id='string' group by create_time order by create_time desc;

所以,可以看到效果实现了。但是实际的业务场景是复杂的,如:

  • 多表查询,直接G

后期具体的效果,也得看我把代码粘贴到项目里,会不会有问题的。shigen后续也会持续观察和分享。

好了,这个夜不敢熬了,dog命要紧。也突然有了困意,新的一天,预祝元气满满!


以上就是今天分享的全部内容了,觉得不错的话,记得点赞 在看 关注支持一下哈,您的鼓励和支持将是shigen坚持日更的动力。同时,shigen在多个平台都有文章的同步,也可以同步的浏览和订阅:

平台账号链接
CSDNshigen01shigen的CSDN主页
知乎gen-2019shigen的知乎主页
掘金shigen01shigen的掘金主页
腾讯云开发者社区shigenshigen的腾讯云开发者社区主页
微信公众平台shigen公众号名:shigen

shigen一起,每天不一样!

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

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

相关文章

手动关闭PS中的TopazStudio2的登录窗口

2021 adobe photoshop Topaz Studio 2 不是使用防火墙出站规则&#xff0c;是手动关闭的解决方案 点击社区-切换用户&#xff0c;登录窗口会出现X&#xff0c;可以手动关闭

VBA根据Excel内容快速创建PPT

示例需求&#xff1a;根据Excel中选中的单元格内容&#xff08;3列&#xff09;如下图所示&#xff0c;在已打卡的PowerPoint文件中创建页面。 新增PPT Slide页面使用第二个模板页面&#xff0c;其中包含两个文本占位符&#xff0c;和一个图片占位符。将Excel选中区域中前两列写…

Nginx 配置详细讲解

Nginx.conf 配置文件分为三部分&#xff0c;分别为main块、events块、http块&#xff08;http块又包含server块和location块&#xff09;&#xff0c;如下图。 第一部分&#xff1a;main块(全局块) main块主要是设置一些影响Nginx服务器整体运行的配置指令&#xff0c;主要包括…

2.8 CSS 伸缩盒模型

1.模型简介 传统布局是指:基于传统盒状模型&#xff0c;主要靠: display 属性 position 属性float 属性。2009年&#xff0c;w3C提出了一种新的盒子模型——Flexible Box(伸缩盒模型&#xff0c;又称:弹性盒子)。它可以轻松的控制:元素分布方式、元素对齐方式、元素视觉顺序..…

0008Java安卓程序设计-ssm基于Android平台的健康管理系统

文章目录 **摘要**目录系统实现开发环境 编程技术交流、源码分享、模板分享、网课教程 &#x1f427;裙&#xff1a;776871563 摘要 首先,论文一开始便是清楚的论述了系统的研究内容。其次,剖析系统需求分析,弄明白“做什么”,分析包括业务分析和业务流程的分析以及用例分析,…

那些看起来高大上的封装函数

什么 ToGray 只支持3通道图像&#xff0c; 让我看看怎么个事 就这么生硬的加了个判断 好家伙 调用了下opencv &#xff0c;通道数都不判断一下

Java多线程面试题:如何在多个子线程中捕获异常并引发主线程异常?

亲爱的小伙伴们&#xff0c;大家好&#xff01;我是你们的小米&#xff0c;今天我要和大家分享一个热门的技术话题——"面试题&#xff1a;多个子线程中&#xff0c;其中一个子线程异常时主线程如何抛出异常并返回&#xff1f;"。这是在面试中经常被问到的问题&#…

【多线程】Lambda表达式

package org.example;public class TestLambda {public static void main(String[] args) {Like likenew Like();like.lambda();}}//定义一个函数式接口 interface ILike{void lambda(); }//实现类 class Like implements ILike{Overridepublic void lambda() {System.out.prin…

Vue Vue3

1、创建VUE3工程 使用vue-cli创建&#xff1a; ## 查看vue/cli版本&#xff0c;确保vue/cli版本在4.5.0以上 vue --version ## 安装或者升级你的vue/cli npm install -g vue/cli ## 创建 vue create vue_test ## 启动 cd vue_test npm run serve 使用vite创建&#xff1a; …

【笔记】单片机卡死的八大原因和解决方法

在微控制器上&#xff0c;程序卡住&#xff08;即停止执行&#xff09;可能有多种原因。下面我将列举一些常见的原因&#xff0c;并提供一些可能导致程序卡住的示例情况。请注意&#xff0c;这里只是一些示例&#xff0c;并不能穷尽所有可能的情况。 1. 死循环&#xff08;Infi…

基于若依的ruoyi-nbcio流程管理系统增加仿钉钉流程设计(六)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 这节主要讲条件节点与并发节点的有效性检查&#xff0c;主要是增加这两个节点的子节点检查&#xff0c;因为…

C/C++输出字符菱形 2021年3月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C输出字符菱形 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C/C输出字符菱形 2021年3月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 给定一个字符&#xff0c;用它构造一个对角线长…

学习c++的第十天

目录 类 & 对象 类定义 对象的建立和使用 构造函数(Constructor) 析构函数(Destructor) 拷贝构造函数 扩展知识 this指针 友元函数的使用方法 友元类的使用方法 常数据的使用及初始化 类 & 对象 什么是类&#xff1f;什么是对象&#xff1f;对于面向对象的…

【黑马程序员】SSM框架——SpringBoot(未完成)

文章目录 前言一、SpringBoot 简介1. 入门案例1.1 入门程序① 创建新模块② 选择当前模块需要使用的技术集③ 开发控制类④ 运行自动生成的 Application 类 1.2 创建 SpringBoot 程序的两种方式1.2.1 最简 SpringBoot 程序所包含的基础文件1.2.2 基于 SpringBoot 官网创建项目 …

vue3后台管理系统之实现分页功能

例子&#xff1a;用户 请求格式 返回数据类型 {"code": 200,"message": "获取所有用户成功","total": 19,"totalPages": 2,"currentPage": 1,"data": [{"id": 1,"username": &qu…

6-8 最宽层次结点数 分数 10

文章目录 1.题目描述2.本题ac答案2.1法一: 代码复用2.2法二: 顺序队列实现层序遍历 3.C层序遍历求最大宽度3.1层序遍历代码3.2求最大宽度 1.题目描述 2.本题ac答案 2.1法一: 代码复用 //二叉树第i层结点个数 int LevelNodeCount(BiTree T, int i) {if (T NULL || i < 1)re…

CleanMyMacX4.16破解版激活码

CleanMyMac X是一款颇受欢迎的专业清理软件&#xff0c;拥有十多项强大的功能&#xff0c;可以进行系统清理、清空废纸篓、清除大旧型文件、程序卸载、除恶意软件、系统维护等等&#xff0c;并且这款清理软件操作简易&#xff0c;非常好上手&#xff0c;特别适用于那些刚入手苹…

卡尔曼家族从零解剖-(04)贝叶斯滤波→细节讨论,逻辑梳理,批量优化

讲解关于slam一系列文章汇总链接:史上最全slam从零开始&#xff0c;针对于本栏目讲解的 卡尔曼家族从零解剖 链接 :卡尔曼家族从零解剖-(00)目录最新无死角讲解&#xff1a;https://blog.csdn.net/weixin_43013761/article/details/133846882 文末正下方中心提供了本人 联系…

C++虚表与虚表指针详解

类的虚表 每个包含了虚函数的类都包含一个虚表。 当一个类&#xff08;B&#xff09;继承另一个类&#xff08;A&#xff09;时&#xff0c;类B会继承类A的函数的调用权。所以如果一个基类包含了虚函数&#xff0c;那么其继承类也可调用这些虚函数&#xff0c;换句话说&…

系统设计中的缓存技术:完整指南

Image.png 缓存是软件工程中用于提高系统性能和用户体验的基本技术。它通过临时存储频繁访问的数据在缓存中&#xff0c;缓存比数据的原始来源更容易访问。 作为一名软件工程师&#xff0c;了解缓存以及它在不同类型的系统中的工作方式是至关重要的。在本文中&#xff0c;我们将…