Spring Boot на практике интегрирует многопользовательскую архитектуру Mybatis-Plus.

Архитектура

В настоящее время продукты компании являются внешними корпоративными сервисами.После прихода в компанию я узнал о модели SaaS и частном развертывании.Когда я впервые услышал о SaaS, я не очень хорошо в этом разбирался. После анализа данных и последующих исследований и разработок функций понимание мультиарендности постоянно углублялось.

Итак, давайте зададим себе несколько вопросов: 1. Что такое многопользовательская архитектура? 2. Решения с многопользовательской архитектурой и их соответствующие преимущества и недостатки? 3. Каковы применимые сценарии многопользовательской архитектуры?

1. Что такое мультиарендность

Мультиарендная технология или мультиарендная технология, называемаяSaaS, представляет собой технологию архитектуры программного обеспечения, которая позволяет совместно использовать одни и те же компоненты системы или программы в многопользовательской среде (многопользовательская среда обычно ориентирована на корпоративных пользователей) и может обеспечить изоляцию данных между пользователями. Проще говоря: один экземпляр приложения, работающий на сервере, который обслуживает несколько арендаторов (клиентов). Из определения мы можем понять, что мультиарендность — это архитектура, целью которой является использование одного и того же набора программ в многопользовательской среде и обеспечение изоляции данных между пользователями. Тогда ключевой момент очень прост и понятен.Ключевой момент мультитенантности заключается в реализации изоляции многопользовательских данных в рамках одного и того же набора программ.

2. Многопользовательская архитектура и схема изоляции данных

Существует три основных решения для мультитенантности в хранилище данных, а именно:

1. Независимая база данных

То есть одна база данных на арендатора.Это решение имеет самый высокий уровень изоляции пользовательских данных и наилучшую безопасность, но стоимость высока.

  • Преимущества: Предоставление независимых баз данных для разных арендаторов, что помогает упростить схему расширения модели данных и удовлетворить уникальные потребности разных арендаторов; в случае сбоя восстановить данные относительно просто.

  • Недостатки: Увеличение количества установок баз данных, что приводит к увеличению затрат на обслуживание и приобретение.

2. Общая база данных, независимая схема

Другими словами, используйте базу данных вместе и используйте таблицы для изоляции данных.Несколько или все арендаторы совместно используют базу данных, но у каждого арендатора есть схема (также называемая пользователем). Например, базовая библиотека: DB2, ORACLE и т. д. В одной базе данных может быть несколько SCHEMA.

  • Преимущества: Обеспечивает определенную степень логической изоляции данных для арендаторов с высокими требованиями к безопасности, а не полную изоляцию; каждая база данных может поддерживать большее количество арендаторов.

  • Недостатки: В случае сбоя восстановление данных затруднено, так как при восстановлении базы данных будут задействованы данные других арендаторов;

3. Общая база данных, общая схема, общая таблица данных

То есть использовать вместе одну базу данных и одну таблицу, а также использовать поля для изоляции данных.

То есть арендаторы совместно используют одну и ту же базу данных и одну и ту же схему, но добавляют в таблицу поле мультитенантных данных TenantID. Это режим с самой высокой степенью совместного использования и самым низким уровнем изоляции.

Проще говоря, каждый раз, когда вы вставляете часть данных, вам нужно иметь идентификатор клиента. Таким образом можно различать данные разных клиентов в одной таблице, что и использует наша система в настоящее время (tenant_id)

  • Преимущества: при сравнении трех схем третья схема имеет самые низкие затраты на обслуживание и приобретение и позволяет максимальное количество арендаторов, поддерживаемых каждой базой данных.

  • Недостатки: уровень изоляции самый низкий, безопасность самая низкая, и объем разработки безопасности должен быть увеличен во время проектирования и разработки; резервное копирование и восстановление данных является наиболее сложным, и его необходимо резервировать и восстанавливать в таблице по стол.

3. Применимые сценарии для многопользовательской архитектуры?

Основными факторами, которые следует учитывать при измерении трех моделей, являются:隔离还是共享. 1. Чем лучше изолированы стоимостные факторы, тем выше сложность и стоимость проектирования и реализации и тем выше первоначальная стоимость. Чем лучше совместное использование, тем больше пользователей поддерживается при тех же эксплуатационных расходах и тем ниже эксплуатационные расходы.

2. Факторы безопасности для учета требований безопасности бизнеса и клиентов. Чем выше требования безопасности, тем больше склонность к изоляции.

3. Учитывайте следующие факторы с точки зрения количества арендаторов

  • Сколько арендаторов будет поддерживать система? Сотни? Тысячи или десятки тысяч? Чем больше возможных жильцов, тем больше склонны делиться.

  • Средний объем пространства, необходимого каждому арендатору для хранения данных. Чем больше данных хранится, тем больше вероятность того, что они будут изолированы.

  • Количество конечных пользователей, одновременно получающих доступ к системе на одного арендатора. Чем больше вам нужно поддерживать, тем больше вы склонны изолировать.

  • Вы хотите предоставить дополнительные услуги для каждого арендатора, такие как резервное копирование и восстановление данных. Чем больше спрос на это, тем больше склонны к изоляции

4. Чем выше разделение технических резервов, тем выше технические требования.

4. Техническая реализация

Выбор технологии: Mybatis-Plus Здесь мы выбираем третье решение (общая база данных, общая схема, общая таблица данных) для реализации, что означает, что каждая таблица данных должна иметь идентификатор клиента (tenant_id).

Теперь есть таблицы базы данных (пользовательские) следующим образом:

имя поля Тип поля описывать
id int(11) первичный ключ
name varchar(30) Имя
tenant_id int(11) многопользовательский идентификатор

Считайте tenant_id идентификатором арендатора, чтобы изолировать данные между арендаторами.Если вы хотите запросить пользователей текущего арендатора, SQL будет примерно следующим:

SELECT * FROM user WHERE tenant_id = 1;скопировать код

Только представьте, в дополнение к некоторым таблицам, разделяемым системой, нам нужно добавить условия запроса AND tenant_id = ? к другим таблицам, связанным с тенантами.Если таблиц данных слишком много, это будет опущено, что приведет к утечке данных. Благодаря плагину mybatis-plus очень удобно реализовать мультитенантный парсер SQL, официальный документ выглядит следующим образом: Мультитенантный парсер SQL

Официально войдите в демоверсию сборки темы Environment

1. Создайте проект Spring Boot

пом-файл:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.1.7.RELEASE</version>        <relativePath/>    </parent>    <groupId>com.xd</groupId>    <artifactId>mybatis-plus-multi-tenancy</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>mybatis-plus-multi-tenancy</name>    <description>基于Spring Boot Mybatis-Plus的多租户架构</description>    <properties>        <java.version>1.8</java.version>        <mybatis-plus.version>3.1.2</mybatis-plus.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <!--mysql-->        <dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>            <scope>runtime</scope>        </dependency>        <!--lombok-->        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <optional>true</optional>        </dependency>        <!--Mybatis-Plus依赖-->        <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-test</artifactId>            <scope>test</scope>        </dependency>        <dependency>            <groupId>junit</groupId>            <artifactId>junit</artifactId>        </dependency>        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-test</artifactId>            <version>5.2.0.M1</version>            <scope>compile</scope>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-test</artifactId>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>скопировать код

application.properties

# 数据源配置spring.datasource.type=com.zaxxer.hikari.HikariDataSourcespring.datasource.hikari.minimum-idle=3spring.datasource.hikari.maximum-pool-size=10# 不能小于30秒,否则默认回到1800秒spring.datasource.hikari.max-lifetime=30000spring.datasource.hikari.connection-test-query=SELECT 1spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/multi?useUnicode=true&characterEncoding=UTF-8spring.datasource.username=rootspring.datasource.password=rootlogging.level.com.xd.mybatisplusmultitenancy=debugскопировать код

Соответствующий файл схемы инициализации базы данных SQL.

SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for user-- ----------------------------DROP TABLE IF EXISTS `user`;CREATE TABLE `user` (  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',  `name` varchar(30) DEFAULT NULL COMMENT '姓名',  `tenant_id` int(11) NOT NULL COMMENT '多租户ID',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;SET FOREIGN_KEY_CHECKS = 1;скопировать код

Конфигурация ядра MybatisPlusConfig: многопользовательский процессор TenantSqlParser.

package com.xd.mybatisplusmultitenancy.config;import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;import lombok.extern.slf4j.Slf4j;import net.sf.jsqlparser.expression.Expression;import net.sf.jsqlparser.expression.LongValue;import net.sf.jsqlparser.expression.NullValue;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;/** * @Classname PreTenantHandler * @Description 租户处理器 -主要实现mybatis-plus https://mp.baomidou.com/guide/tenant.html * @Author Created by Lihaodong (alias:小东啊) lihaodongmail@163.com * @Date 2019-08-09 23:34 * @Version 1.0 */@Slf4j@Componentpublic class MyTenantHandler implements TenantHandler {    /**     * 多租户标识     */    private static final String SYSTEM_TENANT_ID = "tenant_id";    /**     * 需要过滤的表     */    private static final List<String> IGNORE_TENANT_TABLES = new ArrayList<>();    @Autowired    private MyContext apiContext;    /**     * 租户Id     *     * @return     */    @Override    public Expression getTenantId() {        // 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。        Long tenantId = apiContext.getCurrentTenantId();        log.debug("当前租户为{}", tenantId);        if (tenantId == null) {            return new NullValue();        }        return new LongValue(tenantId);    }    /**     * 租户字段名     *     * @return     */    @Override    public String getTenantIdColumn() {        return SYSTEM_TENANT_ID;    }    /**     * 根据表名判断是否进行过滤     * 忽略掉一些表:如租户表(sys_tenant)本身不需要执行这样的处理     *     * @param tableName     * @return     */    @Override    public boolean doTableFilter(String tableName) {        return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));    }}скопировать код

Конфигурация MybatisPlus

package com.xd.mybatisplusmultitenancy.config;import com.baomidou.mybatisplus.core.parser.ISqlParser;import com.baomidou.mybatisplus.extension.parsers.BlockAttackSqlParser;import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;import org.mybatis.spring.annotation.MapperScan;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import net.sf.jsqlparser.expression.Expression;import net.sf.jsqlparser.expression.LongValue;import java.util.ArrayList;import java.util.List;/** * @Classname MybatisPlusConfig * @Description TODO * @Author Created by Lihaodong (alias:小东啊) lihaodongmail@163.com * @Date 2019-08-09 22:44 * @Version 1.0 */@Configuration@MapperScan("com.xd.mybatisplusmultitenancy.mapper")public class MybatisPlusConfig {    @Autowired    private MyTenantHandler myTenantHandler;    @Bean    public PaginationInterceptor paginationInterceptor() {        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();        // SQL解析处理拦截:增加租户处理回调。        List<ISqlParser> sqlParserList = new ArrayList<>();        // 攻击 SQL 阻断解析器、加入解析链        sqlParserList.add(new BlockAttackSqlParser());        // 多租户拦截        TenantSqlParser tenantSqlParser = new TenantSqlParser();        tenantSqlParser.setTenantHandler(myTenantHandler);        sqlParserList.add(tenantSqlParser);        paginationInterceptor.setSqlParserList(sqlParserList);        return paginationInterceptor;    }    /**     * 性能分析拦截器,不建议生产使用     * 用来观察 SQL 执行情况及执行时长     */    @Bean(name = "performanceInterceptor")    public PerformanceInterceptor performanceInterceptor() {        return new PerformanceInterceptor();    }}скопировать код

настраиваемый системный контекст

package com.xd.mybatisplusmultitenancy.config;import org.springframework.stereotype.Component;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/** * @Classname ApiContext * @Description 当前系统的上下文 * @Author Created by Lihaodong (alias:小东啊) lihaodongmail@163.com * @Date 2019-08-09 22:47 * @Version 1.0 */@Componentpublic class MyContext {    private static final String KEY_CURRENT_TENANT_ID = "KEY_CURRENT_PROVIDER_ID";    private static final Map<String, Object> M_CONTEXT = new ConcurrentHashMap<>();    public void setCurrentTenantId(Long tenantId) {        M_CONTEXT.put(KEY_CURRENT_TENANT_ID, tenantId);    }    public Long getCurrentTenantId() {        return (Long) M_CONTEXT.get(KEY_CURRENT_TENANT_ID);    }}скопировать код

Entity, Mapper опущен...

2. Модульное тестирование

package com.xd.mybatisplusmultitenancy.test;import com.baomidou.mybatisplus.core.toolkit.Wrappers;import com.xd.mybatisplusmultitenancy.MybatisPlusMultiTenancyApplication;import com.xd.mybatisplusmultitenancy.config.MyContext;import com.xd.mybatisplusmultitenancy.entity.User;import com.xd.mybatisplusmultitenancy.mapper.UserMapper;import lombok.extern.slf4j.Slf4j;import org.junit.Assert;import org.junit.Before;import org.junit.FixMethodOrder;import org.junit.Test;import org.junit.runner.RunWith;import org.junit.runners.MethodSorters;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.sql.Wrapper;/** * @Classname MybatisPlusMultiTenancyApplicationTests * @Description TODO * @Author Created by Lihaodong (alias:小东啊) lihaodongmail@163.com * @Date 2019-08-09 22:50 * @Version 1.0 */@Slf4j@RunWith(SpringRunner.class)@FixMethodOrder(MethodSorters.JVM)@SpringBootTest(classes = MybatisPlusMultiTenancyApplication.class)public class MybatisPlusMultiTenancyApplicationTests {    @Autowired    private MyContext apiContext;    @Autowired    private UserMapper userMapper;    /**     * 模拟当前系统的多租户Id     */    @Before    public void before() {        // 在上下文中设置当前多租户id        apiContext.setCurrentTenantId(1L);    }    @Test    public void insert() {        // 新增数据        User user = new User().setName("小明");        //判断一个条件是true还是false        Assert.assertTrue(userMapper.insert(user) > 0);        user = userMapper.selectById(user.getId());        log.info("插入数据:{}", user);        // 判断是否相等        Assert.assertEquals(apiContext.getCurrentTenantId(), user.getTenantId());    }    @Test    public void selectList() {        userMapper.selectList(null).forEach((e) -> {            log.info("查询数据{}", e);            Assert.assertEquals(apiContext.getCurrentTenantId(), e.getTenantId());        });    }}скопировать код

вставить данные

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             : 当前租户为12019-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.insertExecute SQL:INSERT INTO user (name, tenant_id) VALUES ('小明', 1)2019-08-23 22:32:53.720 DEBUG 77902 --- [           main] c.x.m.config.MyTenantHandler             : 当前租户为12019-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.selectByIdExecute SQL:SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 1 AND id = 12019-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             : 当前租户为12019-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.selectListExecute SQL:SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 12019-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.скопировать код

Из распечатанного лога нетрудно увидеть, что текущее решение все же относительно совершенно, требует лишь простой настройки, что позволяет разработчикам вести разработку с большим удобством, и при этом в наибольшей степени обеспечивает безопасность данных.

Загрузка исходного кода: https://github.com/LiHaodong888/SpringBootLearn

Конкретные проекты: https://gitee.com/li_haodong/pre

использованная литература

https://www.cnblogs.com/pingfan21/p/7478242.html  https://segmentfault.com/a/1190000017197768скопировать код