本文正在参与「金石计划 . 瓜分6万现金大奖」

铢积寸累,水滴石穿

前语

假如各位小伙伴还不了解 MyBatis Plus的根本运用,请前往:SpringBoot + MyBatisPlus根本运用 或者前往官方文档。

本文就不多逼逼,直接进入正题。

什么是多租户

多租户技能(Multi-TenancyTechnology)又称多重租赁技能,简称SaaS,是一种软件架构技能,是完成如何在多用户环境下 (此处的多用户一般是面向企业用户)共用相同的系统或程序组件,而且可确保各用户间数据的阻隔性。简单讲: 在一台服务器上运转单个运用实例,它为多个租户(客户)供给服务。从定义中咱们能够理解:多租户是一种架 构,意图是为了让多用户环境下运用同一套程序,且保证用户间数据阻隔。那么重点就很深入浅出了,多租户的重 点便是同一套程序下完成多用户数据的阻隔

阻隔计划

现在根据多租户的数据库规划计划一般有如下三种:

  • 1、独立数据库 同享数据库

  • 2、独立 Schema 同享数据库

  • 3、同享数据库、同享数据表

独立数据库

即一个租户一个数据库。

长处

为不同的租户供给独立的数据库,用户数据阻隔等级最高,安全性最好,有助于简化数据模型的扩展规划,满足不同租户的共同需求;假如呈现毛病,康复数据比较简单。

缺陷

数据库维护成本和购置成本的大大增加。

同享数据库,独立 Schema

即多个或一切租户同享Database,每个租户一个Schema。

什么是Schema

  • oracle数据库:在oracle中一个数据库能够具有多个用户,那么一个用户一般对应一个Schema,表都是建立在 Schema 中的,(能够简单的理解:在 oracle 中一个用户一套数据库表)

Mybatis Plus 多租户id使用

  • mysql数据库:mysql数据中的schema比较特别,并不是数据库的下一级,而是等同于数据库。比如履行 create schema test 和履行create database test作用是如出一辙的

长处

为安全性要求较高的租户供给了一定程度的逻辑数据阻隔,并不是彻底阻隔;每个数据库能够支持更多的租户数量。

缺陷

假如呈现毛病,数据康复比较困难,因为康复数据库将牵扯到其他租户的数据;

假如需求跨租户统计数据,存在一定困难。 这种计划是计划一的变种。只需求安装一份数据库服务,经过不同的Schema对不同租户的数据进行阻隔。因为数 据库服务是同享的,所以成本相对低价。

同享数据库、同享数据表

即租户同享同一个Database、同一个Schema,但在表中经过tenant_id字段区分租户的数据,表明该记载是归于哪个租户的。这是同享程度最高、阻隔等级最低的形式。

长处

一切租户运用同一套数据库,所以成本低价。

缺陷

阻隔等级最低,安全性最低,需求在规划开发时加大对安全的开发量;这种计划和根据传统运用的数据库规划并没有任何差异,但是因为一切租户运用相同的数据库表,所以需求做好对 每个租户数据的阻隔安全性处理,这就增加了系统规划和数据管理方面的复杂程度。 数据备份和康复最困难,需求逐表逐条备份和还原。

假如期望以最少的服务器为最多的租户供给服务,而且租户接受以献身阻隔等级换取降低成本,这种计划最适合。

集成

本文选择的是计划三!假如是自己从零开始进行开发,需求在每条 sql 上加上 tenant_id 条件。那开发成本特别大。但咱们运用的是 Mybatis Plus,那就不需求如此复杂了,结构已经集成多租户运用。

创立表

CREATE TABLE `test_tenant` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `account` varchar(32) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `tenant_id` int(10) unsigned DEFAULT NULL COMMENT '租户id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert 句子

INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (1, 'cxyxj', 'cxyxj.qq.com', 0);
INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (2, 'awesome', 'awesome@163.com', 1);
INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (3, 'gongj', 'gongj@163.com', 2);

留意关键字段tenant_id

搭建项目

依靠

搭建 Boot项目,加入以下依靠:

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <mybatis-plus.version>3.5.0</mybatis-plus.version>
</properties>
<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>${mybatis-plus.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

实体

@Data
public class TestTenant {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String account;
    private String email;
    private Integer tenantId;
}

咱们的主键类型为 int,所以需求修正主键战略,修正为自增,默许运用雪花算法生成大局唯一id,长度为19 位。

mapper接口

public interface TenantMapper extends BaseMapper<TestTenant> {
}

MybatisPlusConfig

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                //取得当时登录用户的租户id
                return new LongValue(1111);
            }
        }));
        return interceptor;
    }
}

在 Mybatis Plus 中,一切插件的主体是 InnerInterceptor。 现在已有的功用(官网地址):

  • 自动分页: PaginationInnerInterceptor
  • 多租户: TenantLineInnerInterceptor
  • 动态表名: DynamicTableNameInnerInterceptor
  • 乐观锁: OptimisticLockerInnerInterceptor
  • sql 性能规范: IllegalSQLInnerInterceptor
  • 防止全表更新与删去: BlockAttackInnerInterceptor

本文运用到的是 TenantLineInnerInterceptor。在咱们的代码中,运用了 TenantLineInnerInterceptor 类的有参构造办法。入参为 TenantLineHandler目标。这是比较重要的目标,比如:某一些表不需求拼接多租户条件、多租户的字段名是什么。都是在这个目标中规则。

public interface TenantLineHandler {
    // 取得租户ID值  本文写死了 111
    Expression getTenantId();
   // 数据库字段 默以为 tenant_id
    default String getTenantIdColumn() {
        return "tenant_id";
    }
   // 需求疏忽拼接条件的表名
   // 办法默许返回 false 表明一切表都需求拼多租户条件
    default boolean ignoreTable(String tableName) {
        return false;
    }
  // 这个办法在之前版别是没有的!已给出租户列的 insert 不再拼接条件。运用用户给出的值。
 // 针对比较特别的场景,比如:异步增加时,获取不到登录人的租户ID,则给默许租户ID
    default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
        return columns.stream().map(Column::getColumnName).anyMatch((i) -> {
            return i.equalsIgnoreCase(tenantIdColumn);
        });
    }
}

配置文件

server:
  port: 1998
spring:
  datasource:
    url: jdbc:mysql:/127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8
    username: root
    password: xxx
    driver-class-name: com.mysql.jdbc.Driver
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

测验

@SpringBootTest
public class MybatisPlusApplicationTests {
    @Autowired
    TenantMapper tenantMapper;
    @Test
    public void testSelect() {
        List<TestTenant> testTenants = tenantMapper.selectList(null);
        testTenants.forEach(System.out::println);
    }
}

Mybatis Plus 多租户id使用
重视控制台打印的 sql 句子,在 where 句子后面拼接了 test_tenant.tenant_id = 1111的条件。这说明咱们的租户阻隔到达作用,而且很轻松简单的完成了。

测验增修删句子

那咱们再来看看其他句子!

@Test
public void testOther() {
    System.out.println("测验新增=====");
    TestTenant testTenant = new TestTenant();
    testTenant.setAccount("hhhh");
    testTenant.setEmail("100093");
    tenantMapper.insert(testTenant);
    System.out.println("测验修正=====");
    testTenant.setEmail("@164.com");
    tenantMapper.updateById(testTenant);
    System.out.println("测验删去=====");
    tenantMapper.deleteById(testTenant.getId());
}
  • 测验新增
测验新增=====
==>  Preparing: INSERT INTO test_tenant (account, email, tenant_id) 
VALUES (?, ?, 1111)
==> Parameters: hhhh(String), 100093(String)
<==    Updates: 1
  • 测验修正
测验修正=====
==>  Preparing: UPDATE test_tenant SET account = ?, email = ? WHERE 
test_tenant.tenant_id = 1111 AND id = ?
==> Parameters: hhhh(String), @164.com(String), 4(Integer)
<==    Updates: 1
  • 测验删去
测验删去=====
==>  Preparing: DELETE FROM test_tenant WHERE test_tenant.tenant_id = 1111 AND id = ?
==> Parameters: 4(Integer)
<==    Updates: 1

能够得知,当配置了 TenantLineInnerInterceptor插件后,咱们的 CRUR SQL 都拼接了咱们所指定的字段作为 where 条件。

特别处理

在实际的开发中,必定不会如此的一帆风顺。必定会有一些比较特别的逻辑。

某表不需求拼接租户条件

总有一些表是比较特别的。表中压根就没租户id字段,那这怎么处理呢? 咱们只需求重写 TenantLineHandler 类中的ignoreTable办法即可。

/**
 * 需求疏忽拼接多租户条件的表名
 */
@Value("#{'${mybatis-plus.configuration.ignore-tenant-tables:}'.split(',')}")
private List<String> ignoreTenantTables;
// 该 default 办法 默许返回 false 表明一切表都需求拼多租户条件
// 假如有部分 sql 不需求加上租户ID条件
// 能够运用 @InterceptorIgnore(tenantLine = "true") 标注在 Mapper 接口的办法上
// 而 @SqlParser(filter = true) 在 mybatis-plus 3.4 版别中标记为过期
@Override
public boolean ignoreTable(String tableName) {
    return ignoreTenantTables.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
}

在配置文件中将需求疏忽的表名进行配置。

mybatis-plus:
  configuration:
    ignore-tenant-tables: test_tenant

测验

@Test
public void testSelect() {
    List<TestTenant> testTenants = tenantMapper.selectList(null);
    testTenants.forEach(System.out::println);
}

Mybatis Plus 多租户id使用
能够看到这一次的查询句子中并没有拼接多租户条件。

某一条sql不需求拼接

关于一些拥有租户id字段的表,在某一些场景中,比如:我想取得表中一切数据,不想让它拼接条件。那应该怎么做?留意:这种都是自己自定义 sql 句子。

咱们只需求在自定义的办法上标注一个注解 @InterceptorIgnore。这是官方供给的。

Mybatis Plus 多租户id使用
该注解作用于 xxMapper.java 办法之上,各特点代表对应的插件,各特点不给值则默以为 false,设置为 true 表明疏忽拦截。

自定义 sql

@Select("SELECT id, account, email, tenant_id FROM test_tenant")
@InterceptorIgnore(tenantLine = "true")
List<TestTenant> listAll();

测验

@Test
public void listAll() {
    List<TestTenant> testTenants = tenantMapper.listAll();
    testTenants.forEach(System.out::println);
}

Mybatis Plus 多租户id使用

额外知识点

TenantLineHandler 类中,还有一个办法没有介绍,那便是 ignoreInsert办法。这个办法的作用便是假如你在进行 insert 时,咱们手动给了租户id字段,则结构不再自动拼接。咱们来看看作用吧!

@Test
public void testInsert() {
    System.out.println("测验新增=====");
    TestTenant testTenant = new TestTenant();
    testTenant.setAccount("hhhh");
    testTenant.setEmail("100093");
    testTenant.setTenantId(11232323);
    tenantMapper.insert(testTenant);
}

Mybatis Plus 多租户id使用
能够看到 sql 中 tenant_id 的值,取的是咱们指定的值。 咱们看看源码是怎么处理的!逻辑在 processInsert办法中。

Mybatis Plus 多租户id使用
假如需求插入的列中,包括知道的租户列,则不进行多租户处理。

假如还想对 update、delete sql 也进行这种特别的处理,只需求重写对应的办法 processUpdateprocessDelete


  • 如你对本文有疑问或本文有过错之处,欢迎评论留言指出。如觉得本文对你有所协助,欢迎点赞和重视。