MongoDB多租户方案设计
文章目录
一、前言
二、常见的多租户方案
- DB per tenant
- Schema per tenant
- Discriminator field
三、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();
}
- 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;
}
}
四、调用链
说明:从上面调用可以看出Spring Data Mongo本质是调用MongoTemplate模板类中的方法,所有你也不用担心直接使用MongoTemplate会不会有问题。