一、背景介绍

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

二、基础介绍

  近日有些朋友向我讨教关于多租户设计方案,正好公司做的也是多租户系统,功能实现不是我开发的,之前也没有细致的了解实现过程。借此机会,结合公司现有的多租户方案及其网上浏览的租户方案设计,自己实现了一套技术方案,可以解决共享数据库或共享数据表。

三、数据隔离技术方案

1.独立数据库

  即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。
  优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
  缺点:增多了数据库的安装数量,随之带来维护成本和购置成本的增加。

2.共享数据库,独立 Schema

  多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。底层库比如是:DB2、ORACLE等,一个数据库下可以有多个SCHEMA。
  优点:为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
  缺点:如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;

3.共享数据库,共享 Schema,共享数据表  

  即租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。
  简单来讲,即每插入一条数据时都需要有一个客户的标识。这样才能在同一张表中区分出不同客户的数据,这也是我们系统目前用到的(tenant_id)
  优点:三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。
  缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量; 数据备份和恢复最困难,需要逐表逐条备份和还原。

四、优点介绍

   本次租户业务实现看似是一个方案,其实是结合了独立数据库、共享数据库,共享 Schema,共享数据表类实现的两个方案,具体根据您的需求来设计。

  由于多租户中我们不止是每个租户都存在一个客户标识,可能每个表中都存在创建人、更新人、删除标识,这次我也是集成了方案解决。

     根据自己的需求,灵活变更业务逻辑,业务代码高可用、注释完善、易上手。

     网上的教程大部分都是基于mybatis-plus的TenantLineInnerInterceptor 实现所有的租户通过tenant_id来处理多租户之间打数据隔离,这个局限性太低了,而我实现的可灵活自定义实现。

五、业务实现

  注意:以下实现主要列出核心编码实现,完整代码放在文章最下方。如有不足之处,希望各位IT界大佬多多指教,谢谢!

1.导入maven jar包

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>

2.数据库表,可以建两个库进行模拟

 1 CREATE TABLE `tenant` (
2 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
3 `name` varchar(255) DEFAULT NULL COMMENT '租户名称',
4 `tenant_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '租户id',
5 `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
6 `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建人',
7 `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
8 `update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '修改人',
9 `is_delete` tinyint(1) DEFAULT '0' COMMENT '1删除 0未删除 默认0',
10 PRIMARY KEY (`id`)
11 ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='企业表';

3.我们实现Mybtis中Interceptor,来拦截我们的接口,处理sql。

  注意:handleReplace 方法中的 tenantProperties.getTenantTable()就是获取库的名称,这个需要根据你业务,用户登录后存储租户标识,此处根据不同的用户获取不同的标识,切换数据库,我在本案例中只是指定了某个库区执行sql。


  1 /**
2 * @author: fuzongle
3 * @description: 拦截器
4 **/
5
6 @Slf4j
7 @Intercepts({
8 @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
9 @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
10 })
11 public class SqlLogInterceptor implements Interceptor {
12
13 @Autowired
14 private TenantProperties tenantProperties;
15
16 public static class CustomSqlSource implements SqlSource {
17
18 private BoundSql boundSql;
19
20 protected CustomSqlSource(BoundSql boundSql){
21 this.boundSql = boundSql;
22 }
23
24 @Override
25 public BoundSql getBoundSql(Object o) {
26 return boundSql;
27 }
28 }
29
30 @Override
31 public Object intercept(Invocation invocation) throws Throwable {
32 MappedStatement ms = (MappedStatement)invocation.getArgs()[0];
33 Object parameterObject = null;
34 InterceptorIgnore annotation = null;
35 Class<?> clazz = Class.forName(ms.getId().substring(0, ms.getId().lastIndexOf(".")));
36 Method[] methods = clazz.getDeclaredMethods();
37 for (Method method : methods) {
38 annotation = method.getAnnotation(InterceptorIgnore.class);
39 }
40 if(invocation.getArgs().length > 1){
41 parameterObject = invocation.getArgs()[1];
42 }
43
44 BoundSql boundSql = ms.getBoundSql(parameterObject);
45 String sql = boundSql.getSql();
46
47 if (tenantProperties.getEnable() || annotation == null) {
48 sql = handleReplace(boundSql.getSql());
49 }
50 BoundSql newBoundSql = new BoundSql(
51 ms.getConfiguration(),
52 sql, //sql替换 唯一发生改变的地方
53 boundSql.getParameterMappings(),
54 boundSql.getParameterObject()
55 );
56
57 MappedStatement.Builder build = new MappedStatement.Builder(
58 ms.getConfiguration(),
59 ms.getId(),
60 new CustomSqlSource(newBoundSql),
61 ms.getSqlCommandType()
62 );
63 build.resource(ms.getResource());
64 build.fetchSize(ms.getFetchSize());
65 build.statementType(ms.getStatementType());
66 build.keyGenerator(ms.getKeyGenerator());
67 build.timeout(ms.getTimeout());
68 build.parameterMap(ms.getParameterMap());
69 build.resultMaps(ms.getResultMaps());
70 build.cache(ms.getCache());
71
72 MappedStatement newStmt = build.build();
73 //替换原来的MappedStatement
74 invocation.getArgs()[0] = newStmt;
75
76 return invocation.proceed();
77 }
78    // 核心业务处理
79 private String handleReplace(String sql) throws JSQLParserException {
80 Statement stmt = CCJSqlParserUtil.parse(sql);
81 //需要sql校验。
82 String schemeName = String.format("`%s`", tenantProperties.getTenantTable());
83 if(stmt instanceof Insert){
84 Insert insert = (Insert)stmt;
85 return SqlParser.doInsert(insert, schemeName);
86 }else if(stmt instanceof Update){
87 Update update = (Update) stmt;
88 return SqlParser.doUpdate(update, schemeName);
89 }else if(stmt instanceof Delete){
90 Delete delete = (Delete) stmt;
91 return SqlParser.doDelete(delete, schemeName);
92 }else if(stmt instanceof Select){
93 Select select = (Select)stmt;
94 return SqlParser.doSelect(select, schemeName);
95 }
96 throw new RuntimeException("非法sql!");
97 }
98
99
100
101
102
103 }

4.新增语句处理

 1 public static String doInsert(Insert insert, String schemaName) {
2 String tableName = insert.getTable().getName();
3 //校验系统非追加表
4 if (tenantProperties.getIgnoreTables().contains(tableName)){
5 return insert.toString();
6 }
7 //获取表对应实体路劲
8 String entityPath = EntityTableCache.getInstance().getCacheData(tableName).toString();
9 //判断实体是否有createdBy属性,追加create_by
10 if (EntityUtils.isHaveAttr(entityPath, TenantGlobalColumnHandler.COLUMN_CREATED_BY_ENTITY)) {
11 handleColumnsAndExpressions(insert, TenantGlobalColumnHandler.COLUMN_CREATED_BY);
12 }
13 //判断实体是否有updateBy属性,追加update_by
14 if (EntityUtils.isHaveAttr(entityPath,TenantGlobalColumnHandler.COLUMN_UPDATED_BY_ENTITY)) {
15 handleColumnsAndExpressions(insert,TenantGlobalColumnHandler.COLUMN_UPDATED_BY);
16 }
17 //追加tenant_id
18 insert.getColumns().add(new Column(TenantGlobalColumnHandler.COLUMN_TENANT_ID));
19 ((ExpressionList) insert.getItemsList()).getExpressions().add(TenantGlobalColumnHandler.getTenantId());
20 //是否设置库名
21 Table table = insert.getTable();
22 table.setSchemaName(schemaName);
23 insert.setTable(table);
24 return insert.toString();
25 }

5.删除语句处理

 1 public static String doDelete(Delete delete, String schemaName) {
2 String tableName = delete.getTable().getName();
3 //校验系统非追加表
4 if (tenantProperties.getIgnoreTables().contains(tableName)){
5 return delete.toString();
6 }
7 //构建where条件
8 BinaryExpression binaryExpression = andExpression(delete.getTable(), delete.getWhere());
9 //追加where条件
10 delete.setWhere(binaryExpression);
11 //设置库名
12 Table t = delete.getTable();
13 t.setSchemaName(schemaName);
14 delete.setTable(t);
15 return delete.toString();
16 }

6.修改语句处理 

 1 public static String doUpdate(Update update, String schemaName) throws JSQLParserException{
2 String tableName = update.getTable().getName();
3 //校验系统非追加表
4 if (tenantProperties.getIgnoreTables().contains(tableName)){
5 return update.toString();
6 }
7 //构建where条件
8 BinaryExpression binaryExpression = andExpression(update.getTable(), update.getWhere());
9 //追加where条件
10 update.setWhere(binaryExpression);
11 //获取表对应实体路劲
12 String entityPath = EntityTableCache.getInstance().getCacheData(tableName).toString();
13 //判断实体是否有updateBy属性,追加update_by
14 if (EntityUtils.isHaveAttr(entityPath,TenantGlobalColumnHandler.COLUMN_UPDATED_BY_ENTITY)) {
15 handleColumnsAndExpressions(update,TenantGlobalColumnHandler.COLUMN_UPDATED_BY);
16 }
17
18 //追加库名
19 StringBuilder buffer = new StringBuilder();
20 Table tb = update.getTable();
21 tb.setSchemaName(schemaName);
22 update.setTable(tb);
23 // 处理from
24 FromItem fromItem = update.getFromItem();
25 if (fromItem != null) {
26 Table tf = (Table) fromItem;
27 tf.setSchemaName(schemaName);
28 }
29 // 处理join
30 List<Join> joins = update.getJoins();
31 if (joins != null && joins.size() > 0) {
32 for (Object object : joins) {
33 Join t = (Join) object;
34 Table rightItem = (Table) t.getRightItem();
35 rightItem.setSchemaName(schemaName);
36 System.out.println();
37 }
38 }
39 ExpressionDeParser expressionDeParser = new ExpressionDeParser();
40 UpdateDeParser p = new UpdateDeParser(expressionDeParser, null, buffer);
41 expressionDeParser.setBuffer(buffer);
42 p.deParse(update);
43
44 return update.toString();
45 }

7.查询语句处理,如果不满足您的业务可参考Mybatis Puls中的TenantSqlParser自定义追加条件。

 1 public static String doSelect(Select select, String schemaName){
2 processPlainSelect((PlainSelect) select.getSelectBody());
3 StringBuilder buffer = new StringBuilder();
4 ExpressionDeParser expressionDeParser = new ExpressionDeParser();
5 SQLParserSelect parser = new SQLParserSelect(expressionDeParser, buffer);
6 parser.setSchemaName(schemaName);
7 expressionDeParser.setSelectVisitor(parser);
8 expressionDeParser.setBuffer(buffer);
9 select.getSelectBody().accept(parser);
10
11 return buffer.toString();
12 }

六、执行过程

1.新增

2.删除

3.修改

4.查询

 七、源码地址:

  

Mybatis Plus 多租户架构实现(完美教程)的更多相关文章

  1. Centos7安装并配置mysql5.6完美教程

    Centos7安装并配置mysql5.6完美教程 Centos7将默认数据库mysql替换成了Mariadb,对于我们这些还想使用mysql的开发人员来说并不是一个好消息.然而,网上关于Linux安装 ...

  2. Oracle 12c多租户架构浅析

    Oracle数据库12c的一大创新即是其采用的多租户架构.对于多租户这项新功能,业内的评价褒贬不一.有的声音认为,这项功能的用处不是特别大,但在某些场景或特定的环境下,多租户依然有它的用处.其最大的用 ...

  3. ABP Zero 单部署,单数据库,多租户架构

    首先,我们应该定义多租户系统中的两个条目: 租主(Host):租主是单例的(只有一个租主).租主会对创建和管理租户负责.因此,一个“租主用户”比所有的租户等级更高,并独立于所有租户,同时还能控制他们. ...

  4. Android Navigation 架构组件入门教程

    Android Navigation 架构组件入门教程 版权声明:本文为博主原创文章,未经博主允许不得转载. 转载请表明出处:https://www.cnblogs.com/cavalier-/p/1 ...

  5. Force.com 多租户架构

    本文参考自官方文档. 多租户架构 作为云计算平台的先驱,Salesforce最大的特点是"软件即服务"(Software as a Service,Saas).实现这种技术的基础便 ...

  6. Springmvc+Spring+Mybatis整合开发(架构搭建)

    Springmvc+Spring+Mybatis整合开发(架构搭建) 0.项目结构 Springmvc:web层 Spring:对象的容器 Mybatis:数据库持久化操作 1.导入所有需要的jar包 ...

  7. Intellij Idea下搭建基于Spring+SpringMvc+MyBatis的WebApi接口架构

    2018-08-16 09:27 更新 强烈推荐使用Springboot来搭建MVC框架! 强烈推荐使用Springboot来搭建MVC框架! 强烈推荐使用Springboot来搭建MVC框架! 后文 ...

  8. JAX-RS(基于Jersey) + Spring 4.x + MyBatis构建REST服务架构

    0. 大背景 众所周知,REST架构已经成为现代服务端的趋势. 很多公司,已经采用REST作为App, H5以及其它客户端的服务端架构. 1. 什么是JAX-RS? JAX-RS是JAVA EE6 引 ...

  9. RIA+REST架构实现完美WEB开发

    记得第一次看到REST的身影,是在InfoQ上的一篇介绍,随后又翻阅了后面的参考文章和Developerwork上一些资料,甚至随手翻了翻Roy博士的论文.所幸,在不少人还在体会REST到底是何方神圣 ...

随机推荐

  1. 2-69.x的平方根

    题目描述: 解题思路: 计算平方根可以依次通过自然数递增,来判断两者相乘是否为目标值,是一个有序的序列,因此考虑使用二分查找. 由于x=0和1时,就是其本身,单独拿出来.当x>1时,其平方根一定 ...

  2. 刚转行1年测试新手:学习Python编程经验实战分享

    一.开头说两句 作为一名零基础转行刚一年的测试新手来说,深知自己在技术经验方面落后太多,难免会有急于求成的心态,这也就导致自己在学习新知识时似懂非懂,刚开始学完那会还胸有成竹,一段时间之后却又忘的一干 ...

  3. @Scheduled注解

    1 概述 @Scheduled注解是spring boot提供的用于定时任务控制的注解,主要用于控制任务在某个指定时间执行,或者每隔一段时间执行.注意需要配合@EnableScheduling使用,配 ...

  4. 多图详解 TCP 连接管理,太全了!!!

    TCP 是一种面向连接的单播协议,在 TCP 中,并不存在多播.广播的这种行为,因为 TCP 报文段中能明确发送方和接受方的 IP 地址. 在发送数据前,相互通信的双方(即发送方和接受方)需要建立一条 ...

  5. 记一次 .NET 某教育系统API 异常崩溃分析

    一:背景 1. 讲故事 这篇文章起源于 搬砖队大佬 的精彩文章 WinDBg定位asp.net mvc项目异常崩溃源码位置 ,写的非常好,不过美中不足的是通览全文之后,总觉得有那么一点不过瘾,就是没有 ...

  6. odoo字段属性列举

    罗列一些Odoo中的字段属性,基本包含大部分的属性. 常用字段属性 平平无奇常用字段属性 string:字段的标题,在UI视图标签中使用.它是可选项,如未设置,会通过首字母大写及将空格替换成为下划线来 ...

  7. 分布式任务调度系统:xxl-job

    任务调度,通俗来说实际上就是"定时任务",分布式任务调度系统,翻译一下就是"分布式环境下定时任务系统". xxl-job一个分布式任务调度平台,其核心设计目标是 ...

  8. pwn 好的联系网站

    https://pwnable.kr/ https://w3challs.com/challenges/wargame http://overthewire.org/wargames/ http:// ...

  9. hdu4046 不错的线段树单点更新

    题意:       给一个字符串,两种操作 0 a b 询问a,b之间有多少个wbw, 1 a c 就是把第a个改成c. 思路:       这个题目我们可以用线段树的点更新来做,一开始写了个好长好长 ...

  10. Linux-鸟菜-1-Linux简介

    Linux-鸟菜-1-Linux简介 鸟哥这一章是介绍了Linux的由来以及建议怎么学Linux.很多也是建议性的东西,概念性的东西(当然我不是排斥概念).把最后鸟哥的汇总粘过来吧. 建议大家去看下这 ...