首页 > 解决方案 > Spring boot - 在多租户应用程序中的单个请求中访问多个数据源

问题描述

此应用程序从请求 url 中提取子域,并将其用作tenantId选择要连接的数据源。

public class TenantDetectionFilter extends GenericFilterBean {

    private final MultiTenantManager multiTenantManager;

    public TenantDetectionFilter(MultiTenantManager multiTenantManager) {
        this.multiTenantManager = multiTenantManager;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
        try {
            mapSubDomainToDataSource(getSubDomainFromDomain(servletRequest.getServerName()));
        } catch (Exception e) {
            //sending error
            return;
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    private void mapSubDomainToDataSource(String subDomain) throws Exception {
        multiTenantManager.setCurrentTenant(subDomain);
    }

    private String getSubDomainFromDomain(@NotNull String domain) {
        //logic to extract the sub-domain
    }
}

该类MultiTenantManager使用提取的子域将应用程序映射到相关的 MySql 数据库。


@Configuration
public class MultiTenantManager {

    public static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
    private final Map<Object, Object> tenantDataSources = new ConcurrentHashMap<>();
    private static final String DB_CONNECTOR_DRIVER = "com.mysql.cj.jdbc.Driver";
    private AbstractRoutingDataSource multiTenantDataSource;

    @Bean
    public DataSource dataSource() {
        multiTenantDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                return currentTenant.get();
            }
        };
        multiTenantDataSource.setTargetDataSources(tenantDataSources);
        multiTenantDataSource.setDefaultTargetDataSource(defaultDataSource());
        multiTenantDataSource.afterPropertiesSet();
        populateTenantsProd();

        return multiTenantDataSource;
    }

    public void addTenant(String tenantId, String url, String username, String password) throws SQLException {
        DataSource dataSource = DataSourceBuilder.create()
                                                 .driverClassName(DB_CONNECTOR_DRIVER)
                                                 .url(url)
                                                 .username(username)
                                                 .password(password)
                                                 .build();

        try (Connection c = dataSource.getConnection()) {
            tenantDataSources.put(tenantId, dataSource);
            multiTenantDataSource.afterPropertiesSet();
        }
    }

    public void setCurrentTenant(String tenantId) throws Exception {
        currentTenant.set(tenantId);
    }

    private DriverManagerDataSource defaultDataSource() {
        DriverManagerDataSource defaultDataSource = new DriverManagerDataSource();
        defaultDataSource.setDriverClassName(DB_CONNECTOR_DRIVER);
        defaultDataSource.setUrl("jdbc:mysql://localhost:3306/defaultDB");
        defaultDataSource.setUsername("username");
        defaultDataSource.setPassword("password)");
        return defaultDataSource;
    }

    private void populateTenantsProd() {
        try {
            addTenant("tenantId_1", "jdbc:mysql://localhost:3306/mysql_db_1", "username", "password");
            addTenant("tenantId_2", "jdbc:mysql://localhost:3306/mysql_db_2", "username", "password");
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
}

到目前为止,一切运作良好。但是在场景中添加了一个新要求,即也使用MongoDB数据库。因此,当收到请求时,逻辑代码应该能够使用选定的(使用tenantIdMysql数据库来处理它的业务逻辑,而且它应该能够使用选定的(使用tenantId)MongoDB 数据库来保存一些元数据。

我将Spring Data JpaHibernate一起使用。

  1. 从域ex中提取子域:tenant_1
  2. 为此解析 MongoDB 和 Mysql 数据库名称tenantId
  3. 在业务逻辑中使用两个数据库

您能否解释一下如何在 Spring Boot 中实现这一目标。

标签: javaspring-bootspring-data-jpamulti-tenant

解决方案


我必须重写getDb(String dbName)才能完成这项工作。

public class MongoFactory extends SimpleMongoDbFactory
{

    private static final String DB_PREFIX = "prefix_";

    public MongoFactory(MongoClientURI uri)
    {
        super(uri);
    }

    public MongoFactory()
    {
        super(new MongoClientURI("mongodb://localhost/test"));
    }

    @Override
    public MongoDatabase getDb(String dbName) throws DataAccessException
    {
        if(Objects.nonNull(currentTenant.get())){
            return super.getDb(DB_PREFIX+currentTenant.get());
        }
        return super.getDb();
    }
}

而在MultiTenantManager,

@Bean(name = "mongoDbFactory")
public MongoDbFactory mongoDbFactory()throws Exception
{
    return new MongoFactory();
}

@Bean
public MongoTemplate mongoTemplate(@Qualifier("mongoDbFactory") MongoDbFactory dbFactory) throws Exception
{
    return new MongoTemplate(dbFactory);
}

推荐阅读