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会不会有问题。

在Java语言中实现MongoDB Collection的多租户隔离,可以通过使用不同的Collection前缀或后缀来实现。例如,为每个租户创建一个前缀或后缀,然后在代码中使用该前缀或后缀来访问该租户的Collection。 具体步骤如下: 1. 创建多个MongoDB Collection,并为每个Collection添加相应的前缀或后缀,以区分不同的租户。 2. 在Java代码中,使用MongoClient连接到MongoDB,并使用相应的用户名和密码进行身份验证。 3. 在代码中实现访问控制,确保每个用户只能访问其所拥有的Collection。 例如,以下代码演示了如何使用Java语言实现MongoDB Collection的多租户隔离: ``` MongoClient mongoClient = new MongoClient("localhost", 27017); // 创建多个MongoDB Collection,并为每个Collection添加相应的前缀或后缀 MongoCollection<Document> collection1 = mongoClient.getDatabase("mydb").getCollection("tenant1_collection"); MongoCollection<Document> collection2 = mongoClient.getDatabase("mydb").getCollection("tenant2_collection"); // 使用相应的用户名和密码连接到MongoDB MongoCredential credential = MongoCredential.createCredential("user1", "mydb", "password".toCharArray()); // 在代码中实现访问控制,确保每个用户只能访问其所拥有的Collection if (isTenant1) { MongoCollection<Document> collection = mongoClient.getDatabase("mydb").getCollection("tenant1_collection"); } else if (isTenant2) { MongoCollection<Document> collection = mongoClient.getDatabase("mydb").getCollection("tenant2_collection"); } ``` 通过以上步骤,就可以使用Java语言实现MongoDB Collection的多租户隔离。注意,在实现访问控制时,需要确保每个用户只能访问其所拥有的Collection,以保证多租户数据的安全性。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搬山境KL攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值