You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring多租户架构中DataSource与Repository循环依赖问题

解决Spring多租户架构中的循环依赖问题

问题场景

在Spring应用中实现多租户架构时,通过ConfigurationDataSource类在启动时构建AbstractRoutingDataSource,代码如下:

import com.example.hikaricp_demo.domain.TenantConfig;
import com.example.hikaricp_demo.repository.TenantConfigRepository;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
public class ConfigurationDataSource {

    @Autowired
    private TenantConfigRepository repository;

    private List<TenantConfig> getListTenant(){
        return repository.findAll();
    }

    TenantConfig tenantDefault() {
        return repository.findByTenantId("tenantDefault");
    }

    @Bean
    public DataSource dataSource() {
        Map<Object, Object> resolvedDataSources = new HashMap<>();
        List<TenantConfig> listTenant = getListTenant();

        for (TenantConfig item : listTenant) {
            HikariConfig hikariConfig = new HikariConfig();
            hikariConfig.setDriverClassName(item.getDriverClassName());
            hikariConfig.setJdbcUrl(item.getUrl());
            hikariConfig.setUsername(item.getUsername());
            hikariConfig.setPassword(item.getPassword());
            hikariConfig.setSchema(item.getSchema());

            HikariDataSource dataSource = new HikariDataSource(hikariConfig);
            resolvedDataSources.put(item.getTenantId(), dataSource);
        }

        AbstractRoutingDataSource dataSource = new MultiTenantDataSource();
        dataSource.setDefaultTargetDataSource(tenantDefault());
        dataSource.setTargetDataSources(resolvedDataSources);
        dataSource.afterPropertiesSet();
        return dataSource;
    }
}

其中TenantConfigRepository是Spring Data JPA仓库,用于从数据库读取租户配置。启动时出现循环依赖错误:

Description:

The dependencies of some of the beans in the application context form a cycle:

   entityManagerFactory defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]
┌─────┐
|  dataSourceScriptDatabaseInitializer defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]
↑     ↓
|  configurationDataSource (field private com.example.hikaricp_demo.repository.TenantConfigRepository com.example.hikaricp_demo.config.ConfigurationDataSource.repository)
↑     ↓
|  tenantConfigRepository defined in com.example.hikaricp_demo.repository.TenantConfigRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration
↑     ↓
|  jpaSharedEM_entityManagerFactory
└─────┘

原因分析

循环依赖的核心是:DataSource Bean需要TenantConfigRepository加载租户配置,而TenantConfigRepository作为JPA仓库,又依赖DataSource来初始化,形成闭环。

解决方案

通过分离系统数据源和多租户数据源打破循环:专门配置一个系统数据源用于存储租户配置,先初始化该数据源和对应的JPA组件,再用它加载租户配置构建多租户数据源。

1. 配置系统数据源(存储租户配置)

创建SystemDataSourceConfig类,单独配置连接租户配置库的数据源、EntityManagerFactory和事务管理器,与业务组件隔离:

import com.example.hikaricp_demo.domain.TenantConfig;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.example.hikaricp_demo.repository",
        entityManagerFactoryRef = "systemEntityManagerFactory",
        transactionManagerRef = "systemTransactionManager"
)
public class SystemDataSourceConfig {

    @Primary
    @Bean
    @ConfigurationProperties("spring.datasource.system")
    public DataSourceProperties systemDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Primary
    @Bean
    public DataSource systemDataSource(@Qualifier("systemDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }

    @Primary
    @Bean(name = "systemEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean systemEntityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier("systemDataSource") DataSource dataSource) {
        Map<String, String> jpaProperties = new HashMap<>();
        jpaProperties.put("hibernate.hbm2ddl.auto", "update");
        return builder
                .dataSource(dataSource)
                .packages(TenantConfig.class)
                .persistenceUnit("system")
                .properties(jpaProperties)
                .build();
    }

    @Primary
    @Bean(name = "systemTransactionManager")
    public PlatformTransactionManager systemTransactionManager(
            @Qualifier("systemEntityManagerFactory") LocalContainerEntityManagerFactoryBean systemEntityManagerFactory) {
        return new JpaTransactionManager(systemEntityManagerFactory.getObject());
    }
}

在配置文件中添加系统数据源的连接信息(以YAML为例):

spring:
  datasource:
    system:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/tenant_config_db?useSSL=false&serverTimezone=UTC
      username: root
      password: root

2. 修正多租户数据源配置

修改ConfigurationDataSource,修复默认数据源的错误(原代码直接传入TenantConfig对象),并确保注入系统数据源对应的TenantConfigRepository:

import com.example.hikaricp_demo.domain.TenantConfig;
import com.example.hikaricp_demo.repository.TenantConfigRepository;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
public class ConfigurationDataSource {

    @Autowired
    private TenantConfigRepository repository;

    private List<TenantConfig> getListTenant(){
        return repository.findAll();
    }

    TenantConfig tenantDefault() {
        return repository.findByTenantId("tenantDefault");
    }

    @Bean(name = "multiTenantDataSource")
    public DataSource dataSource() {
        Map<Object, Object> resolvedDataSources = new HashMap<>();
        List<TenantConfig> listTenant = getListTenant();

        for (TenantConfig item : listTenant) {
            HikariConfig hikariConfig = new HikariConfig();
            hikariConfig.setDriverClassName(item.getDriverClassName());
            hikariConfig.setJdbcUrl(item.getUrl());
            hikariConfig.setUsername(item.getUsername());
            hikariConfig.setPassword(item.getPassword());
            hikariConfig.setSchema(item.getSchema());

            HikariDataSource dataSource = new HikariDataSource(hikariConfig);
            resolvedDataSources.put(item.getTenantId(), dataSource);
        }

        AbstractRoutingDataSource dataSource = new MultiTenantDataSource();
        // 为默认租户创建对应的DataSource
        TenantConfig defaultTenant = tenantDefault();
        HikariConfig defaultConfig = new HikariConfig();
        defaultConfig.setDriverClassName(defaultTenant.getDriverClassName());
        defaultConfig.setJdbcUrl(defaultTenant.getUrl());
        defaultConfig.setUsername(defaultTenant.getUsername());
        defaultConfig.setPassword(defaultTenant.getPassword());
        defaultConfig.setSchema(defaultTenant.getSchema());
        dataSource.setDefaultTargetDataSource(new HikariDataSource(defaultConfig));
        
        dataSource.setTargetDataSources(resolvedDataSources);
        dataSource.afterPropertiesSet();
        return dataSource;
    }
}

3. 配置业务JPA组件使用多租户数据源

创建BusinessJpaConfig,让业务Repository和实体类使用多租户数据源:

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.example.hikaricp_demo.business.repository",
        entityManagerFactoryRef = "businessEntityManagerFactory",
        transactionManagerRef = "businessTransactionManager"
)
public class BusinessJpaConfig {

    @Bean(name = "businessEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean businessEntityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier("multiTenantDataSource") DataSource dataSource) {
        Map<String, String> jpaProperties = new HashMap<>();
        return builder
                .dataSource(dataSource)
                .packages("com.example.hikaricp_demo.business.domain")
                .persistenceUnit("business")
                .properties(jpaProperties)
                .build();
    }

    @Bean(name = "businessTransactionManager")
    public PlatformTransactionManager businessTransactionManager(
            @Qualifier("businessEntityManagerFactory") LocalContainerEntityManagerFactoryBean businessEntityManagerFactory) {
        return new JpaTransactionManager(businessEntityManagerFactory.getObject());
    }
}

原理说明

通过分离系统数据源和多租户数据源,构建了线性的依赖链:

  1. 系统数据源先初始化 → 初始化TenantConfigRepository
  2. 用TenantConfigRepository加载租户配置 → 构建多租户数据源
  3. 业务JPA组件依赖多租户数据源完成初始化

彻底打破了原有的循环依赖,同时修正了原代码中默认数据源设置错误的问题。

内容的提问来源于stack exchange,提问作者Thanh Dat Truong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 23:54:55