当前位置:   article > 正文

Mybatis-plus多租户实现方案_mybatisplus 多租户

mybatisplus 多租户

一、什么是多租户?

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

二. 多租户架构以及数据隔离方案

1. 独立数据库

即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。

  • 优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
  • 缺点:增多了数据库的安装数量,随之带来维护成本和购置成本的增加。

2. 共享数据库,独立 Schema

也就是说 共同使用一个数据库 使用表进行数据隔离
多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。底层库比如是:DB2、ORACLE等,一个数据库下可以有多个SCHEMA。

优点:为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。

缺点:如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;

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

也就是说 共同使用一个数据库一个表 使用字段进行数据隔离

即租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。

简单来讲,即每插入一条数据时都需要有一个客户的标识。这样才能在同一张表中区分出不同客户的数据,这也是我们系统目前用到的(tenant_id)

  • 优点:三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。
  • 缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;数据备份和恢复最困难,需要逐表逐条备份和还原。

三.多租户架构适用场景?

衡量三种模式主要考虑的因素是隔离还是共享
1.成本角度因素
隔离性越好,设计和实现的难度和成本越高,初始成本越高。共享性越好,同一运营成本下支持的用户越多,运营成本越低。

2.安全因素
要考虑业务和客户的安全方面的要求。安全性要求越高,越要倾向于隔离。

3.从租户数量上考虑
主要考虑下面一些因素

  • 系统要支持多少租户?上百?上千还是上万?可能的租户越多,越倾向于共享。
  • 平均每个租户要存储数据需要的空间大小。存贮的数据越多,越倾向于隔离。
  • 每个租户的同时访问系统的最终用户数量。需要支持的越多,越倾向于隔离。
  • 是否想针对每一租户提供附加的服务,例如数据的备份和恢复等。这方面的需求越多, 越倾向于隔离

4.技术储备
共享性越高,对技术的要求越高。

四. 技术实现

技术选型: Mybatis-Plus
这里我们选用了第三种方案(共享数据库,共享 Schema,共享数据表)来实现,也就意味着,每个数据表都需要有一个租户标识(tenant_id)

现在有数据库表(user)如下:

字段名字段类型描述
idint(11)主键
namevarchar(30)姓名
tenant_idint(11)多租户id

将tenant_id视为租户ID,用来隔离租户与租户之间的数据,如果要查询当前租户的用户,SQL大致如下:

SELECT * FROM user WHERE tenant_id = 1;

试想一下,除了一些系统共用的表以外,其他租户相关的表,我们都需要加上AND tenant_id = ?查询条件,数据表多的情况时就会漏加导致数据泄露。
幸亏有mybatis-plus这个插件,可以极为方便的实现多租户SQL解析器,官方文档如下:
多租户 SQL 解析器

具体实现

环境搭建演示

1. 创建Spring Boot项目

pom.xml 文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-parent</artifactId>
  8. <version>2.1.7.RELEASE</version>
  9. <relativePath/>
  10. </parent>
  11. <groupId>com.xd</groupId>
  12. <artifactId>mybatis-plus-multi-tenancy</artifactId>
  13. <version>0.0.1-SNAPSHOT</version>
  14. <name>mybatis-plus-multi-tenancy</name>
  15. <description>基于Spring Boot Mybatis-Plus的多租户架构</description>
  16. <properties>
  17. <java.version>1.8</java.version>
  18. <mybatis-plus.version>3.1.2</mybatis-plus.version>
  19. </properties>
  20. <dependencies>
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-web</artifactId>
  24. </dependency>
  25. <!--mysql-->
  26. <dependency>
  27. <groupId>mysql</groupId>
  28. <artifactId>mysql-connector-java</artifactId>
  29. <scope>runtime</scope>
  30. </dependency>
  31. <!--lombok-->
  32. <dependency>
  33. <groupId>org.projectlombok</groupId>
  34. <artifactId>lombok</artifactId>
  35. <optional>true</optional>
  36. </dependency>
  37. <!--Mybatis-Plus依赖-->
  38. <dependency>
  39. <groupId>com.baomidou</groupId>
  40. <artifactId>mybatis-plus-boot-starter</artifactId>
  41. <version>${mybatis-plus.version}</version>
  42. </dependency>
  43. <!--测试相关依赖-->
  44. <dependency>
  45. <groupId>org.springframework.boot</groupId>
  46. <artifactId>spring-boot-starter-test</artifactId>
  47. <scope>test</scope>
  48. </dependency>
  49. <dependency>
  50. <groupId>junit</groupId>
  51. <artifactId>junit</artifactId>
  52. </dependency>
  53. <dependency>
  54. <groupId>org.springframework</groupId>
  55. <artifactId>spring-test</artifactId>
  56. <version>5.2.0.M1</version>
  57. <scope>compile</scope>
  58. </dependency>
  59. <dependency>
  60. <groupId>org.springframework.boot</groupId>
  61. <artifactId>spring-boot-test</artifactId>
  62. </dependency>
  63. </dependencies>
  64. <build>
  65. <plugins>
  66. <plugin>
  67. <groupId>org.springframework.boot</groupId>
  68. <artifactId>spring-boot-maven-plugin</artifactId>
  69. </plugin>
  70. </plugins>
  71. </build>
  72. </project>

application.properties

  1. # 数据源配置
  2. spring.datasource.type=com.zaxxer.hikari.HikariDataSource
  3. spring.datasource.hikari.minimum-idle=3
  4. spring.datasource.hikari.maximum-pool-size=10
  5. # 不能小于30秒,否则默认回到1800秒
  6. spring.datasource.hikari.max-lifetime=30000
  7. spring.datasource.hikari.connection-test-query=SELECT 1
  8. spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  9. spring.datasource.url=jdbc:mysql://localhost:3306/multi?useUnicode=true&characterEncoding=UTF-8
  10. spring.datasource.username=root
  11. spring.datasource.password=root
  12. logging.level.com.xd.mybatisplusmultitenancy=debug

对应的SQL数据库初始化schema文件

  1. SET NAMES utf8mb4;
  2. SET FOREIGN_KEY_CHECKS = 0;
  3. -- ----------------------------
  4. -- Table structure for user
  5. -- ----------------------------
  6. DROP TABLE IF EXISTS `user`;
  7. CREATE TABLE `user` (
  8. `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  9. `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  10. `tenant_id` int(11) NOT NULL COMMENT '多租户ID',
  11. PRIMARY KEY (`id`)
  12. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  13. SET FOREIGN_KEY_CHECKS = 1;

MybatisPlusConfig
核心配置:TenantSqlParser
多租户处理器

  1. package com.xd.mybatisplusmultitenancy.config;
  2. import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
  3. import lombok.extern.slf4j.Slf4j;
  4. import net.sf.jsqlparser.expression.Expression;
  5. import net.sf.jsqlparser.expression.LongValue;
  6. import net.sf.jsqlparser.expression.NullValue;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Component;
  9. import java.util.ArrayList;
  10. import java.util.List;
  11. /**
  12. * @Classname PreTenantHandler
  13. * @Date 2019-08-09 23:34
  14. * @Version 1.0
  15. */
  16. @Slf4j
  17. @Component
  18. public class MyTenantHandler implements TenantHandler {
  19. /**
  20. * 多租户标识
  21. */
  22. private static final String SYSTEM_TENANT_ID = "tenant_id";
  23. /**
  24. * 需要过滤的表
  25. */
  26. private static final List<String> IGNORE_TENANT_TABLES = new ArrayList<>();
  27. @Autowired
  28. private MyContext apiContext;
  29. /**
  30. * 租户Id
  31. *
  32. * @return
  33. */
  34. @Override
  35. public Expression getTenantId() {
  36. // 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
  37. Long tenantId = apiContext.getCurrentTenantId();
  38. log.debug("当前租户为{}", tenantId);
  39. if (tenantId == null) {
  40. return new NullValue();
  41. }
  42. return new LongValue(tenantId);
  43. }
  44. /**
  45. * 租户字段名
  46. *
  47. * @return
  48. */
  49. @Override
  50. public String getTenantIdColumn() {
  51. return SYSTEM_TENANT_ID;
  52. }
  53. /**
  54. * 根据表名判断是否进行过滤
  55. * 忽略掉一些表:如租户表(sys_tenant)本身不需要执行这样的处理
  56. *
  57. * @param tableName
  58. * @return
  59. */
  60. @Override
  61. public boolean doTableFilter(String tableName) {
  62. return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
  63. }
  64. }

MybatisPlus的配置

  1. package com.xd.mybatisplusmultitenancy.config;
  2. import com.baomidou.mybatisplus.core.parser.ISqlParser;
  3. import com.baomidou.mybatisplus.extension.parsers.BlockAttackSqlParser;
  4. import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
  5. import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
  6. import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
  7. import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
  8. import org.mybatis.spring.annotation.MapperScan;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.context.annotation.Bean;
  11. import org.springframework.context.annotation.Configuration;
  12. import net.sf.jsqlparser.expression.Expression;
  13. import net.sf.jsqlparser.expression.LongValue;
  14. import java.util.ArrayList;
  15. import java.util.List;
  16. /**
  17. * @Classname MybatisPlusConfig
  18. * @Description TODO
  19. * @Date 2019-08-09 22:44
  20. * @Version 1.0
  21. */
  22. @Configuration
  23. @MapperScan("com.xd.mybatisplusmultitenancy.mapper")
  24. public class MybatisPlusConfig {
  25. @Autowired
  26. private MyTenantHandler myTenantHandler;
  27. @Bean
  28. public PaginationInterceptor paginationInterceptor() {
  29. PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
  30. // SQL解析处理拦截:增加租户处理回调。
  31. List<ISqlParser> sqlParserList = new ArrayList<>();
  32. // 攻击 SQL 阻断解析器、加入解析链
  33. sqlParserList.add(new BlockAttackSqlParser());
  34. // 多租户拦截
  35. TenantSqlParser tenantSqlParser = new TenantSqlParser();
  36. tenantSqlParser.setTenantHandler(myTenantHandler);
  37. sqlParserList.add(tenantSqlParser);
  38. paginationInterceptor.setSqlParserList(sqlParserList);
  39. return paginationInterceptor;
  40. }
  41. /**
  42. * 性能分析拦截器,不建议生产使用
  43. * 用来观察 SQL 执行情况及执行时长
  44. */
  45. @Bean(name = "performanceInterceptor")
  46. public PerformanceInterceptor performanceInterceptor() {
  47. return new PerformanceInterceptor();
  48. }
  49. }

自定义系统的上下文

  1. package com.xd.mybatisplusmultitenancy.config;
  2. import org.springframework.stereotype.Component;
  3. import java.util.Map;
  4. import java.util.concurrent.ConcurrentHashMap;
  5. /**
  6. * @Classname ApiContext
  7. * @Description 当前系统的上下文
  8. * @Date 2019-08-09 22:47
  9. * @Version 1.0
  10. */
  11. @Component
  12. public class MyContext {
  13. private static final String KEY_CURRENT_TENANT_ID = "KEY_CURRENT_PROVIDER_ID";
  14. private static final Map<String, Object> M_CONTEXT = new ConcurrentHashMap<>();
  15. public void setCurrentTenantId(Long tenantId) {
  16. M_CONTEXT.put(KEY_CURRENT_TENANT_ID, tenantId);
  17. }
  18. public Long getCurrentTenantId() {
  19. return (Long) M_CONTEXT.get(KEY_CURRENT_TENANT_ID);
  20. }
  21. }

Entity、Mapper 省略...

2. 单元测试

  1. package com.xd.mybatisplusmultitenancy.test;
  2. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  3. import com.xd.mybatisplusmultitenancy.MybatisPlusMultiTenancyApplication;
  4. import com.xd.mybatisplusmultitenancy.config.MyContext;
  5. import com.xd.mybatisplusmultitenancy.entity.User;
  6. import com.xd.mybatisplusmultitenancy.mapper.UserMapper;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.junit.Assert;
  9. import org.junit.Before;
  10. import org.junit.FixMethodOrder;
  11. import org.junit.Test;
  12. import org.junit.runner.RunWith;
  13. import org.junit.runners.MethodSorters;
  14. import org.springframework.beans.factory.annotation.Autowired;
  15. import org.springframework.boot.test.context.SpringBootTest;
  16. import org.springframework.test.context.junit4.SpringRunner;
  17. import java.sql.Wrapper;
  18. /**
  19. * @Classname MybatisPlusMultiTenancyApplicationTests
  20. * @Description TODO
  21. * @Date 2019-08-09 22:50
  22. * @Version 1.0
  23. */
  24. @Slf4j
  25. @RunWith(SpringRunner.class)
  26. @FixMethodOrder(MethodSorters.JVM)
  27. @SpringBootTest(classes = MybatisPlusMultiTenancyApplication.class)
  28. public class MybatisPlusMultiTenancyApplicationTests {
  29. @Autowired
  30. private MyContext apiContext;
  31. @Autowired
  32. private UserMapper userMapper;
  33. /**
  34. * 模拟当前系统的多租户Id
  35. */
  36. @Before
  37. public void before() {
  38. // 在上下文中设置当前多租户id
  39. apiContext.setCurrentTenantId(1L);
  40. }
  41. @Test
  42. public void insert() {
  43. // 新增数据
  44. User user = new User().setName("小明");
  45. //判断一个条件是true还是false
  46. Assert.assertTrue(userMapper.insert(user) > 0);
  47. user = userMapper.selectById(user.getId());
  48. log.info("插入数据:{}", user);
  49. // 判断是否相等
  50. Assert.assertEquals(apiContext.getCurrentTenantId(), user.getTenantId());
  51. }
  52. @Test
  53. public void selectList() {
  54. userMapper.selectList(null).forEach((e) -> {
  55. log.info("查询数据{}", e);
  56. Assert.assertEquals(apiContext.getCurrentTenantId(), e.getTenantId());
  57. });
  58. }
  59. }

 

运行结果

插入数据

2019-08-23 22:32:52.755 INFO 77902 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-08-23 22:32:53.210 INFO 77902 --- [ main] .MybatisPlusMultiTenancyApplicationTests : Started MybatisPlusMultiTenancyApplicationTests in 5.181 seconds (JVM running for 6.86)
2019-08-23 22:32:53.613 DEBUG 77902 --- [ main] c.x.m.config.MyTenantHandler : 当前租户为1
2019-08-23 22:32:53.614 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user (name, tenant_id) VALUES (?, 1)
2019-08-23 22:32:53.648 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.insert : ==> Parameters: 小明(String)
2019-08-23 22:32:53.701 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.insert : <== Updates: 1
Time:64 ms - ID:com.xd.mybatisplusmultitenancy.mapper.UserMapper.insert
Execute SQL:INSERT INTO user (name, tenant_id) VALUES ('小明', 1)

2019-08-23 22:32:53.720 DEBUG 77902 --- [ main] c.x.m.config.MyTenantHandler : 当前租户为1
2019-08-23 22:32:53.722 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.selectById : ==> Preparing: SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 1 AND id = ?
2019-08-23 22:32:53.726 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.selectById : ==> Parameters: 1(Long)
2019-08-23 22:32:53.745 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.selectById : <== Total: 1
Time:20 ms - ID:com.xd.mybatisplusmultitenancy.mapper.UserMapper.selectById
Execute SQL:SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 1 AND id = 1

2019-08-23 22:32:53.746 INFO 77902 --- [ main] .MybatisPlusMultiTenancyApplicationTests : 插入数据:User(id=1, name=小明, tenantId=1)
2019-08-23 22:32:53.762 INFO 77902 --- [ Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2019-08-23 22:32:53.764 INFO 77902 --- [ Thread-2] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2019-08-23 22:32:53.777 INFO 77902 --- [ Thread-2] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.

 查询数据

2019-08-23 22:34:26.700  INFO 77922 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-08-23 22:34:27.100  INFO 77922 --- [           main] .MybatisPlusMultiTenancyApplicationTests : Started MybatisPlusMultiTenancyApplicationTests in 4.521 seconds (JVM running for 6.268)
2019-08-23 22:34:27.412 DEBUG 77922 --- [           main] c.x.m.config.MyTenantHandler             : 当前租户为1
2019-08-23 22:34:27.414 DEBUG 77922 --- [           main] c.x.m.mapper.UserMapper.selectList       : ==>  Preparing: SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 1
2019-08-23 22:34:27.442 DEBUG 77922 --- [           main] c.x.m.mapper.UserMapper.selectList       : ==> Parameters:
2019-08-23 22:34:27.464 DEBUG 77922 --- [           main] c.x.m.mapper.UserMapper.selectList       : <==      Total: 1
 Time:22 ms - ID:com.xd.mybatisplusmultitenancy.mapper.UserMapper.selectList
Execute SQL:SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 1

2019-08-23 22:34:27.467  INFO 77922 --- [           main] .MybatisPlusMultiTenancyApplicationTests : 查询数据User(id=1, name=小明, tenantId=1)
2019-08-23 22:34:27.480  INFO 77922 --- [       Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
2019-08-23 22:34:27.482  INFO 77922 --- [       Thread-2] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2019-08-23 22:34:27.492  INFO 77922 --- [       Thread-2] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

从打印的日志不难看出,目前这个方案还是比较完美的,仅需简单的配置,让开发者极大方便的进行开发,同时又最大程度的保证了数据的安全性

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/407489
推荐阅读
相关标签
  

闽ICP备14008679号