IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> MongoDB多租户方案设计 -> 正文阅读

[大数据]MongoDB多租户方案设计

MongoDB多租户方案设计

一、前言

多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。简单来说是指一个单独的实例可以为多个组织服务。 在多租户技术的加持下,服务提供商不必为每个组织单独部署一套数据库、应用服务程序,首先节省了服务器等硬件资源,其次软件服务的运维工作也将变得简单,最后还可以结合虚拟机化技术或容器技术充分最大化利用硬件资源,节约成本
SaaS,是Software-as-a-Service的缩写名称,意思为软件即服务,即通过网络提供软件服务。
多重租赁技术是SaaS的重要特性,近几年国内SaaS热度不断攀升。其实SaaS解决方案并未无懈可击,比如SaaS企业个性化技术不是很成熟、数据安全性相比私有化部署差距甚远等等。

二、常见的多租户方案

  • DB per tenant

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

  • Schema per tenant

即多个或所有租户共享Database,但一个Tenant一个Schema。

  • Discriminator field

即租户共享同一个Database、同一个Schema,但在表中通过TenantID区分租户的数据。这是共享程度最高、隔离级别最低的模式。

对于MySQL数据库应用来讲,通常采用第三种方案,通过在表中增加TenantID区分租户的数据,如果ORM框架使用的是Mybatis,你可以通过自定义SQL拦截器,实现租户字段TenantID自动补全,也可以通过开源框架Mybatis-Plus的多租户插件。
而对于MongoDB数据库,由于我本人接触的时间也不是很长,目前暂未找到进行自定义语句拦截的开源方案,打算通过动态切换MongoDB的方式实现租户资源切换。如果你打算自行开发一套MongoDB多租户拦截插件,你需要对MongoDB语法树和执行引擎比较熟悉,相比动态切换MongoDB来说难度较大。

三、MongoDB 多租户方案

CentOS 7.9 MongoDB 安装和使用

1.pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

2.application.yml

spring:
  data:
    mongodb:
      host: localhost
      port: 27017
      database: tenant-default
      username: admin
      password: 123456
      authentication-database: admin
      auto-index-creation: false
logging:
  level:
    org.springframework.data.mongodb.core: debug

3.multi-mongo-spring-boot-starter

D:.
│  pom.xml
│
├─src
│  └─main
│      ├─java
│      │  └─com
│      │      └─example
│      │          └─demo
│      │              └─mongo
│      │                  ├─autoconfigure
│      │                  │      MongoMultiTenantAutoConfiguration.java 自动配置类
│      │                  │
│      │                  ├─context
│      │                  │      MongoContextHolder.java ThreadLocal DB上下文
│      │                  │
│      │                  ├─factory
│      │                  │      MongoMultiTenantFactory.java  MongoDB数据库工厂类(非连接工厂) 
│      │                  │
│      │                  ├─filter
│      │                  │      MongoContextFilter.java Web Filter
│      │                  │      OrderedMongoContextFilter.java Ordered Web Filter
│      │                  │
│      │                  └─provider
│      │                          MongoMultiTenantNameProvider.java 多租户DB名称提供者(接口)
│      │
│      └─resources
│          └─META-INF
│                  spring.factories

4.代码

  • MongoMultiTenantAutoConfiguration
package com.example.demo.mongo.autoconfigure;

import com.example.demo.mongo.factory.MongoMultiTenantFactory;
import com.example.demo.mongo.filter.MongoContextFilter;
import com.example.demo.mongo.filter.OrderedMongoContextFilter;
import com.example.demo.mongo.helper.MongoMultiTenantHelper;
import com.example.demo.mongo.provider.MongoMultiTenantNameProvider;
import com.mongodb.client.MongoClient;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoDatabaseFactorySupport;
import org.springframework.data.mongodb.core.MongoTemplate;

@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(MongoAutoConfiguration.class)
public class MongoMultiTenantAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(MongoDatabaseFactory.class)
    MongoDatabaseFactorySupport<?> mongoDatabaseFactory(MongoClient mongoClient, MongoProperties properties) {
        return new MongoMultiTenantFactory(mongoClient, properties.getMongoClientDatabase());
    }

    /**
     * 默认MongoDB租户数据库
     *
     * @param properties MongoDB配置
     * @return MongoDB租户数据库名
     */
    @Bean
    @ConditionalOnMissingBean(MongoMultiTenantNameProvider.class)
    public MongoMultiTenantNameProvider defaultTenantProvider(MongoProperties properties) {
        return properties::getDatabase;
    }

    /**
     * 线程上下文(租户)
     *
     * @param mongoMultiTenantNameProvider MongoDB租户数据库名提供者
     * @return 线程上下文(租户)Filter
     */
    @Bean
    @ConditionalOnWebApplication
    @ConditionalOnBean(MongoMultiTenantNameProvider.class)
    @ConditionalOnMissingBean(MongoContextFilter.class)
    @ConditionalOnMissingFilterBean(MongoContextFilter.class)
    public MongoContextFilter mongodbContextFilter(MongoMultiTenantNameProvider mongoMultiTenantNameProvider) {
        return new OrderedMongoContextFilter(mongoMultiTenantNameProvider);
    }

}

  • MongoContextHolder
package com.example.demo.mongo.context;

import com.example.demo.mongo.filter.MongoContextFilter;

/**
 * MongoDB上下文对象
 *
 */
public abstract class MongoContextHolder {

    private static final ThreadLocal<String> context = new InheritableThreadLocal<>();

    public static void setDbName(String dbName) {
        context.set(dbName);
    }

    public static String getDbName() {
        return context.get();
    }

    public static void reset() {
        context.remove();
    }

}

  • MongoMultiTenantFactory
package com.example.demo.mongo.factory;

import com.example.demo.mongo.context.MongoContextHolder;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.util.StringUtils;

public class MongoMultiTenantFactory extends SimpleMongoClientDatabaseFactory {

    private static final Logger logger = LoggerFactory.getLogger(MongoMultiTenantFactory.class);

    public MongoMultiTenantFactory(MongoClient mongoClient, String databaseName) {
        super(mongoClient, databaseName);
    }

    /**
     * 切换租户MongoDB数据库
     *
     * @param dbName 数据库名称
     * @return 租户MongoDB数据库
     */
    @Override
    protected MongoDatabase doGetMongoDatabase(String dbName) {
        // 从线程上下文获取MongoDB数据库名称 
        final String context = MongoContextHolder.getDbName();
        String target = dbName;
        if (StringUtils.hasLength(context)) {
            target = context;
            logger.debug("MongoDB switch to {}", context);
        }
        return super.doGetMongoDatabase(target);
    }

}
  • MongoContextFilter
package com.example.demo.mongo.filter;

import com.example.demo.mongo.context.MongoContextHolder;
import com.example.demo.mongo.provider.MongoMultiTenantNameProvider;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 线程上下文(租户)
 *
 */
public class MongoContextFilter extends OncePerRequestFilter {

    private MongoMultiTenantNameProvider mongoMultiTenantNameProvider;

    public MongoContextFilter(MongoMultiTenantNameProvider mongoMultiTenantNameProvider) {
        this.mongoMultiTenantNameProvider = mongoMultiTenantNameProvider;
    }

    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }

    @Override
    protected boolean shouldNotFilterErrorDispatch() {
        return false;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        initContextHolders();
        try {
            filterChain.doFilter(request, response);
        } finally {
            resetContextHolders();
        }
    }

    private void initContextHolders() {
        final String dbName = mongoMultiTenantNameProvider.getTenantMongodbName();
        MongoContextHolder.setDbName(dbName);
    }

    private void resetContextHolders() {
        MongoContextHolder.reset();
    }

}

  • OrderedMongoContextFilter
package com.example.demo.mongo.filter;

import com.example.demo.mongo.provider.MongoMultiTenantNameProvider;
import org.springframework.core.Ordered;

/**
 * 线程上下文(租户)
 *
 */
public class OrderedMongoContextFilter extends MongoContextFilter implements Ordered {

    private int order = Ordered.LOWEST_PRECEDENCE;

    public OrderedMongoContextFilter(MongoMultiTenantNameProvider mongoMultiTenantNameProvider) {
        super(mongoMultiTenantNameProvider);
    }

    @Override
    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

}
  • MongoMultiTenantNameProvider
package com.example.demo.mongo.provider;

public interface MongoMultiTenantNameProvider {
    /**
     * 获取数据库
     * @return 数据库名
     */
    String getTenantMongodbName();
}

MongoDB租户数据库名提供者,根据业务情况自定义,可以简单的将租户ID作为数据库,也可以根据租户ID经过一定的处理逻辑生成一个唯一的数据库名称。

  • MongoConfiguration
package com.example.demo.bussiness.config;

import com.example.demo.common.context.TenantContextHolder;
import com.example.demo.mongo.provider.MongoMultiTenantNameProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MongoConfiguration {

    /**
     * @return MongoDB租户数据库名提供者
     */
    @Bean
    public MongoMultiTenantNameProvider mongodbTenantProvider() {
        return TenantContextHolder::getTenant;
    }
}

四、调用链

Web Request (以查询数据为例)

=>自定义Web Filter,请求拦截顺序默认最低

com.example.demo.mongo.filter.MongoContextFilter#doFilterInternal

=>通过多租户DB名称提供者获取数据库名称(仅提供Function接口,需要业务方实现具体方法,并将其注入到Spring容器中,例如可以简单的将租户ID作为DB名称TenantContextHolder::getTenant

com.example.demo.mongo.provider.MongoMultiTenantNameProvider#getTenantMongodbName

=>向ThreadLocal DB上下文写入数据库名称

com.example.demo.mongo.context.MongoContextHolder#setDbName

=> 调用Spring Data Mongo API

org.springframework.data.mongodb.repository.MongoRepository#findAll()

=>

org.springframework.data.mongodb.repository.support.SimpleMongoRepository#findAll(org.springframework.data.mongodb.core.query.Query)

=> MongoTemplate作为mongoOperations的默认实现类注入到Spring容器中

org.springframework.data.mongodb.core.MongoTemplate#find(org.springframework.data.mongodb.core.query.Query, java.lang.Class<T>, java.lang.String)

=> 省略MongoTemplate内部调用部分

org.springframework.data.mongodb.core.MongoTemplate#doGetDatabase

=> 使用MongoDatabaseUtils工具类,从MongoDatabaseFactory工厂类获取MongoDatabase

org.springframework.data.mongodb.MongoDatabaseUtils#getDatabase(org.springframework.data.mongodb.MongoDatabaseFactory, org.springframework.data.mongodb.SessionSynchronization)

=>调用抽象类MongoDatabaseFactorySupport方法

org.springframework.data.mongodb.core.MongoDatabaseFactorySupport#getMongoDatabase(java.lang.String)

=>调用MongoDatabaseFactorySupport具体实现类(自定义的MongoMultiTenantFactory.java)的doGetMongoDatabase方法

com.example.demo.mongo.factory.MongoMultiTenantFactory#doGetMongoDatabase

=>从ThreadLocal DB上下文获取数据库名称

com.example.demo.mongo.context.MongoContextHolder#getDbName

=>省略中间一大部分调用代码

=>继续执行MongoContextFilter剩余代码

com.example.demo.mongo.filter.MongoContextFilter#doFilterInternal

=>清理ThreadLocal DB上下文

com.example.demo.mongo.context.MongoContextHolder#reset

=>返回数据

说明:从上面调用可以看出Spring Data Mongo本质是调用MongoTemplate模板类中的方法,所有你也不用担心直接使用MongoTemplate会不会有问题。

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-03-13 21:52:47  更:2022-03-13 21:55:04 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 7:47:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码