zoukankan      html  css  js  c++  java
  • Mybatis Plus 多租户架构实现(完美教程)

    一、背景介绍

      多租户技术或称多重租赁技术,简称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包

     1 <dependencies>
     2         <dependency>
     3             <groupId>org.springframework.boot</groupId>
     4             <artifactId>spring-boot-starter-jdbc</artifactId>
     5         </dependency>
     6         <dependency>
     7             <groupId>org.springframework.boot</groupId>
     8             <artifactId>spring-boot-starter-web</artifactId>
     9         </dependency>
    10 
    11         <dependency>
    12             <groupId>mysql</groupId>
    13             <artifactId>mysql-connector-java</artifactId>
    14             <scope>runtime</scope>
    15         </dependency>
    16         <dependency>
    17             <groupId>org.springframework.boot</groupId>
    18             <artifactId>spring-boot-starter-test</artifactId>
    19             <scope>test</scope>
    20         </dependency>
    21         <dependency>
    22             <groupId>org.apache.commons</groupId>
    23             <artifactId>commons-lang3</artifactId>
    24         </dependency>
    25         <!--阿里数据库连接池 -->
    26         <dependency>
    27             <groupId>com.alibaba</groupId>
    28             <artifactId>druid-spring-boot-starter</artifactId>
    29             <version>1.1.21</version>
    30         </dependency>
    31         <dependency>
    32             <groupId>com.baomidou</groupId>
    33             <artifactId>mybatis-plus-boot-starter</artifactId>
    34             <version>3.4.1</version>
    35         </dependency>
    36         <dependency>
    37             <groupId>org.projectlombok</groupId>
    38             <artifactId>lombok</artifactId>
    39             <optional>true</optional>
    40         </dependency>
    41         <dependency>
    42             <groupId>com.baomidou</groupId>
    43             <artifactId>mybatis-plus-extension</artifactId>
    44             <version>3.4.1</version>
    45         </dependency>
    46     </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.查询

     七、源码地址:https://gitee.com/fuzongle

       

  • 相关阅读:
    《Cracking the Coding Interview》——第7章:数学和概率论——题目4
    《Cracking the Coding Interview》——第7章:数学和概率论——题目3
    《Cracking the Coding Interview》——第7章:数学和概率论——题目2
    最小二乘拟合
    设置手机邮件下载文件路径
    #pragma data_seg() 共享数据// MyData段 // 进程 // DLL
    树状数组板子 x
    博弈论 x
    luogu P1147 连续自然数和 x
    luogu P1068 分数线划定 x
  • 原文地址:https://www.cnblogs.com/fuzongle/p/14822697.html
Copyright © 2011-2022 走看看