如何实现多租户系统

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层的两个接口方法:

    @TenantUser selectByPrimaryKey(Long id);@TenantList<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'";@Overridepublic 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.selectByPrimaryKeyString id = mappedStatement.getId();// sql语句类型 select、delete、insert、updateSqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();BoundSql boundSql = statementHandler.getBoundSql();// 获取到原始sql语句 带?号String sql = boundSql.getSql().toLowerCase();// 注解逻辑判断  添加注解了才拦截// 如:com.gitee.shigen.mapper.UserMapperClass<?> classType = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf(".")));// selectByPrimaryKeyString 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 {// 不存在wheresb.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的语法都检查不过,就别提数据的隔离了。

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

    @TenantList<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 countFROM userGROUP BY create_timeORDER BY create_time DESC;</select>

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

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

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

接口测试
@RestController
@RequestMapping(value = "user")
public class UserController {@Resourceprivate UserMapper userMapper;@GetMapping(value = "{id}")public User getById(@PathVariable("id") Long id) {return userMapper.selectByPrimaryKey(id);}@GetMappingpublic 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.rhkb.cn/news/180492.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux flock和fcntl函数详解

文章目录 flock函数描述返回值和错误码笔记 fcntl函数描述复制文件描述符文件描述标志文件状态标志 咨询锁强制锁管理信号租赁文件和目录变更通知改变管道容量 返回值错误备注遗留问题 flock函数 主要功能是在已打开的文件应用或者删除共享锁或者独占锁。sys/file.h声明了这个…

使用Postman工具做接口测试 —— 环境变量与请求参数格式!

引言 在上一篇笔记我们主要介绍了接口测试的基础知识与基本功能&#xff0c;本章主要介绍如何使用postman做接口测试。 配置环境变量和全局变量 环境变量和全局变量 环境管理中还可以点击“Global”添加全局变量&#xff0c;环境变量只有当选择了该环境时才生效&#xff0c;…

centos9 stream 下 rabbitmq高可用集群搭建及使用

RabbitMQ是一种常用的消息队列系统&#xff0c;可以快速搭建一个高可用的集群环境&#xff0c;以提高系统的弹性和可靠性。下面是搭建RabbitMQ集群的步骤&#xff1a; 基于centos9 stream系统 1. 安装Erlang和RabbitMQ 首先需要在所有节点上安装Erlang和RabbitMQ。建议使用官…

VueJs各个版本— 判断当前是开发、生产环境

VueJs各个版本— 判断当前是开发、生产环境 文章目录 VueJs各个版本— 判断当前是开发、生产环境vue项目分类VueCLI21&#xff0c;判断样例2&#xff0c;判断原理 Vue CLI 3 和 Vue CLI 41&#xff0c;判断样例2, 判断原理手动设置-json文件手动设置- .env 文件单个 .env 文件多…

SpringBoot-SpringCache缓存

文章目录 Spring Cache 介绍常用注解 Spring Cache 介绍 Spring Cache 是一个框架&#xff0c;实现了基于注解的缓存功能&#xff0c;只需要简单地加一个注解&#xff0c;就能实现缓存功能。 Spring Cache 提供了一层抽象&#xff0c;底层可以切换不同的缓存实现&#xff0c;…

shell脚本代码混淆

文章目录 起因安装 Bashfuscator安装BashfuscatorBashfuscator的使用 起因 很多时候我并不希望自己的shell脚本被别人看到&#xff0c;于是我在想有没有什么玩意可以把代码加密而又正常执行&#xff0c;于是我想到了代码混淆&#xff0c;简单来看一下&#xff1a; 现在我的目…

JavaEE平台技术——预备知识(Maven、Docker)

JavaEE平台技术——预备知识&#xff08;Maven、Docker&#xff09; 1. Maven2. Docker 在观看这个之前&#xff0c;大家请查阅前序内容。 &#x1f600;JavaEE的渊源 &#x1f600;&#x1f600;JavaEE平台技术——预备知识&#xff08;Web、Sevlet、Tomcat&#xff09; 1. M…

Git 内容学习

一、Git 的理解 Git是一个分布式版本控制系统&#xff08;Distributed Version Control System&#xff0c;简称 DVCS&#xff09;&#xff0c;用于对项目源代码进行管理和跟踪变更。分为两种类型的仓库&#xff1a;本地仓库和远程仓库。 二、Git 的工作流程 详解如下&#x…

高性能渲染——详解Html Canvas的优势与性能

本文由葡萄城技术团队原创并首发。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 一、什么是Canvas 想必学习前端的同学们对Canvas 都不陌生&#xff0c;它是 HTML5 新增的“画布”元素&#x…

GPT引发智能AI时代潮流

最近GPT概念爆火&#xff0c;许多行业开始竞相发展AI &#xff0c;工作就业也将面临跳转&#xff0c;目前测试就业形势就分为了两大类&#xff0c;一类是测试行业如功能、性能、自动化综合性人才就业技能需求&#xff0c;另一类便是AI测试行业的需求普遍增长&#xff0c;原本由…

一键混剪软件、根据模板批量剪辑、多种分割、多种合并、多种混剪、文案提取、文字转语音等

在这个短视频时代&#xff0c;视频剪辑已经成为了一个炙手可热的行业。但是&#xff0c;对于大多数人来说&#xff0c;视频剪辑是一项既复杂又繁琐的工作。不过&#xff0c;现在有了我们的新伙伴——视频闪闪&#xff0c;一键混剪软件&#xff0c;您将迎来全新的视频剪辑体验&a…

axios 实现请求重试

前景提要&#xff1a; ts 简易封装 axios&#xff0c;统一 API 实现在 config 中配置开关拦截器 请求重试的核心是可以重放请求&#xff0c;具体实现就是在 axios 中&#xff0c;拿到当前请求的 config 对象&#xff0c;再用 axios 实例&#xff0c;就能重放请求。 在无感刷新…

HCIA数据通信——路由协议

数据通信——网络层&#xff08;OSPF基础特性&#xff09;_咕噜跳的博客-CSDN博客 数据通信——网络层&#xff08;RIP与BGP&#xff09;_咕噜跳的博客-CSDN博客 上述是之前写的理论知识部分&#xff0c;懒得在实验中再次提及了。这次做RIP协议以及OSPF协议。不过RIP协议不常用…

Zabbix如何监控腾讯云NAT网关

1、NAT网关介绍 NAT 网关&#xff08;NAT Gateway&#xff09;是一种支持 IP 地址转换服务&#xff0c;提供网络地址转换能力&#xff0c;主要包括SNAT&#xff08;Source Network Address Translation&#xff0c;源网络地址转换&#xff09;和DNAT&#xff08;Destination N…

通过在Z平面放置零极点的来设计数字滤波器

文章来源地址&#xff1a;https://www.yii666.com/blog/393376.html 通过在Z平面放置零极点的来设计数字滤波器 要求&#xff1a;设计一款高通滤波器&#xff0c;用在音频信号处理过程中&#xff0c;滤掉100Hz以下的信号。 实现方法&#xff1a;通过在Z平面放置零极点的来设…

由于flutter_app依赖于flutter_swiper>=0.0.2,不支持零安全,版本解决失败。

参考 dart3.0使用flutter_swiper报错记录 flutter_swiper package - All Versions从官网的信息可以看到 Dart3版本不兼容 最小兼容的Dart SDK版本需要2.0 Flutter SDK 版本列表Flutter SDK 版本列表 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter 说明&#xff1a;因…

青翼科技-国产化ARM系列TES720D-KIT

板卡概述 TES720D-KIT是专门针对我司TES720D&#xff08;基于复旦微FMQL20S400的全国产化ARM核心板&#xff09;的一套开发套件&#xff0c;它包含1个TES720D核心板&#xff0c;加上一个TES720D-EXT扩展底板。 FMQL20S400是复旦微电子研制的全可编程融合芯片&#xff0c;在单…

Envoy XDS协议学习

Envoy xds学习 资料地址 envoy官网资料连接 接口说明 xds分为增量接口和全量接口SotW&#xff1a;state of the world 即全量的数据Incremental&#xff1a; 增量的数据 具体接口 Listener: Listener Discovery Service (LDS) SotW: ListenerDiscoveryService.StreamList…

第9章_子查询

文章目录 1 需求分析与问题解决1.1 实际问题1.2 子查询的基本使用1.3 子查询的分类1.3.1 分类方式11.3.2 分类方式2 1.4 演示代码 2 单行子查询2.1 单行比较操作符2.2 代码示例2.3 HAVING中的子查询2.4 CASE中的子查询2.5 子查询中的空值问题2.6 非法使用子查询演示代码 3 多行…

06.Oracle数据备份与恢复

Oracle数据备份与恢复 一、通过RMAN方式备份二、使用emp/imp和expdb/impdb工具进行备份和恢复三、使用Data guard进行备份与恢复 一、通过RMAN方式备份 通过 RMAN&#xff08;Oracle 数据库备份和恢复管理器&#xff09;方式备份 Oracle 数据库&#xff0c;可以使用以下步骤&a…