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

Spring Boot如何实现多库同时连接及运行时动态新增数据库?

问题背景

我们正在将项目迁移至Spring Boot,这个Web应用需要连接2个预先已知的组织数据库,以及初始未知的客户数据库——客户数据库名称通过API传入,且API会被并发调用,因此需要支持动态连接不同数据库。我自己写了一段可运行的代码,但觉得实现方式不对,想请教三个问题:

  1. 不将LocalContainerEntityManagerFactoryBean作为Bean使用是否可行?
  2. 如何通过它创建Repository?
  3. 是否有更优的实现方案?

附当前代码:

public class ConfigClass {
    public static SortedMap<String, WeakReference<EntityManagerFactory>> factoryMap = Collections.synchronizedSortedMap(new TreeMap<String, WeakReference<EntityManagerFactory>>());


    public DataSource getDataSource(String dbname)
    {
        
        return DataSourceBuilder
                .create()
                .username("root")
                .url("jdbc:mysql://localhost/"+dbname)
                .password("root")
                .build();
    }


    public LocalContainerEntityManagerFactoryBean getLocalContainerEntityManagerFactoryBean (DataSource dataSource) {

        System.out.println(dataSource);

        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean =
                new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource);
        entityManagerFactoryBean.setPackagesToScan("com.example.entity");
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();

        Properties jpaProperties = new Properties();
        jpaProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
        jpaProperties.setProperty("hibernate.show_sql", "true");
        jpaProperties.setProperty("hibernate.format_sql", "true");
        jpaProperties.setProperty("hibernate.hbm2ddl.auto", "update");
        entityManagerFactoryBean.setJpaProperties(jpaProperties);

        entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
        entityManagerFactoryBean.afterPropertiesSet();
        return entityManagerFactoryBean;
    }

    public EntityManagerFactory getEntityManagerFactory(LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean) throws SQLException {
        System.out.println(localContainerEntityManagerFactoryBean.getDataSource().getConnection());
        return localContainerEntityManagerFactoryBean.getObject();
    }
    public EntityManager getEntityManager(EntityManagerFactory entityManagerFactory)
    {
        System.out.println(entityManagerFactory);
        return entityManagerFactory.createEntityManager();
    }
    public EntityManager getEM(String dbname) throws SQLException {

        if(factoryMap.containsKey(dbname)&&factoryMap.get(dbname).get()!=null) {
            System.out.println("From factoryMap");
            return factoryMap.get(dbname).get().createEntityManager();
        }
        System.out.println("creating Entitymanagerfactory");
        factoryMap.put(dbname,new WeakReference<>( getEntityManagerFactory(getLocalContainerEntityManagerFactoryBean(getDataSource(dbname)))));
        return factoryMap.get(dbname).get().createEntityManager();
    }
}

问题1:不将LocalContainerEntityManagerFactoryBean作为Bean使用是否可行?

可行,但会丢失Spring容器的核心管理能力:

  • LocalContainerEntityManagerFactoryBean是Spring提供的FactoryBean,负责创建和管理EntityManagerFactory的生命周期。如果不注册为Spring Bean,Spring不会自动处理它的销毁、资源释放逻辑,你得手动维护,容易出现连接泄漏、资源未回收的问题。
  • 你当前手动调用afterPropertiesSet()虽然能完成初始化,但后续的配置调整、异常处理都得自己写逻辑,没有Spring的自动兜底。
  • 另外,用WeakReference缓存EntityManagerFactory可能导致对象被GC意外回收,引发频繁创建销毁的性能损耗,并发场景下这个问题会更明显。

问题2:如何通过它创建Repository?

Spring Data JPA的Repository默认和固定EntityManagerFactory绑定,要动态绑定不同实例,可按以下方式实现:

  1. 手动实例化Repository:自定义Repository接口继承JpaRepository,用SimpleJpaRepository实现类手动创建实例,传入对应数据库的EntityManager:
public <T, ID> JpaRepository<T, ID> getRepository(EntityManager em, Class<T> entityClass) {
    return new SimpleJpaRepository<>(entityClass, em);
}

调用示例:

// 获取对应客户库的Customer实体Repository
EntityManager em = getEM("customer_db_01");
JpaRepository<Customer, Long> customerRepo = getRepository(em, Customer.class);
  1. 手动管理事务:动态获取的EntityManager不在Spring声明式事务范围内,需要手动控制事务:
EntityManager em = getEM("customer_db_01");
EntityTransaction tx = em.getTransaction();
try {
    tx.begin();
    // 执行数据库操作
    customerRepo.save(new Customer());
    tx.commit();
} catch (Exception e) {
    if (tx.isActive()) tx.rollback();
    throw e;
} finally {
    em.close();
}

问题3:更优的实现方案

针对固定多库+动态客户库的场景,推荐两种成熟方案:

方案1:基于AbstractRoutingDataSource的动态路由数据源

适合所有数据库结构一致的场景:

  1. 配置固定组织数据源:将两个已知的组织数据库注册为Spring Bean,配置对应的基础连接信息。
  2. 实现动态路由数据源:继承AbstractRoutingDataSource,通过ThreadLocal存储当前要使用的数据源标识(如客户库名称),重写determineCurrentLookupKey()返回标识:
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> CURRENT_DB = new ThreadLocal<>();

    public static void setCurrentDb(String dbName) {
        CURRENT_DB.set(dbName);
    }

    public static void clearCurrentDb() {
        CURRENT_DB.remove();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return CURRENT_DB.get();
    }
}
  1. 初始化路由数据源:在配置类中把固定组织数据源加入路由,同时提供动态添加客户数据源的方法:
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
        // 初始化固定组织数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("org_db1", createOrgDb1DataSource());
        targetDataSources.put("org_db2", createOrgDb2DataSource());
        routingDataSource.setTargetDataSources(targetDataSources);
        // 设置默认数据源
        routingDataSource.setDefaultTargetDataSource(createOrgDb1DataSource());
        return routingDataSource;
    }

    // 动态添加客户数据源
    public void addCustomerDataSource(String dbName) {
        DynamicRoutingDataSource routingDs = (DynamicRoutingDataSource) dynamicDataSource();
        if (!routingDs.getTargetDataSources().containsKey(dbName)) {
            DataSource customerDs = DataSourceBuilder.create()
                    .username("root")
                    .url("jdbc:mysql://localhost/" + dbName)
                    .password("root")
                    .build();
            routingDs.addTargetDataSource(dbName, customerDs);
            routingDs.afterPropertiesSet();
        }
    }

    // 封装固定组织数据源创建逻辑
    private DataSource createOrgDb1DataSource() {
        return DataSourceBuilder.create()
                .username("root")
                .url("jdbc:mysql://localhost/org_db1")
                .password("root")
                .build();
    }
}
  1. 业务层使用:在API方法中切换数据源标识,直接用Spring Data JPA的Repository操作:
@GetMapping("/customer/{dbName}")
public List<Customer> getCustomers(@PathVariable String dbName) {
    DynamicRoutingDataSource.setCurrentDb(dbName);
    try {
        return customerRepository.findAll();
    } finally {
        DynamicRoutingDataSource.clearCurrentDb();
    }
}

方案2:Hibernate多租户模式

适合典型多租户场景(每个客户一个独立数据库):

  1. 配置多租户策略:在JPA配置中开启Hibernate多租户支持,指定租户标识解析器和数据源提供者:
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
public class MultiTenantJpaConfig {
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setPackagesToScan("com.example.entity");
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);

        Properties properties = new Properties();
        properties.put("hibernate.multiTenancy", "DATABASE");
        properties.put("hibernate.tenant_identifier_resolver", tenantIdentifierResolver());
        properties.put("hibernate.multi_tenant_connection_provider", multiTenantConnectionProvider());
        // 其他Hibernate配置
        properties.put("hibernate.show_sql", "true");
        em.setJpaProperties(properties);
        return em;
    }

    // 从请求中获取租户标识(这里假设从请求头获取)
    @Bean
    public TenantIdentifierResolver tenantIdentifierResolver() {
        return () -> {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            return request.getHeader("X-DB-Name");
        };
    }

    // 动态提供数据源并缓存
    @Bean
    public MultiTenantConnectionProvider multiTenantConnectionProvider() {
        return new AbstractMultiTenantConnectionProvider() {
            private final Map<String, DataSource> dataSourceCache = new ConcurrentHashMap<>();

            // 初始化固定组织数据源
            {
                dataSourceCache.put("org_db1", createDataSource("org_db1"));
                dataSourceCache.put("org_db2", createDataSource("org_db2"));
            }

            private DataSource createDataSource(String dbName) {
                return DataSourceBuilder.create()
                        .username("root")
                        .url("jdbc:mysql://localhost/" + dbName)
                        .password("root")
                        .build();
            }

            @Override
            protected Connection getAnyConnection() throws SQLException {
                return dataSourceCache.values().iterator().next().getConnection();
            }

            @Override
            protected void releaseAnyConnection(Connection connection) throws SQLException {
                connection.close();
            }

            @Override
            protected Connection getConnection(String tenantIdentifier) throws SQLException {
                if (!dataSourceCache.containsKey(tenantIdentifier)) {
                    dataSourceCache.put(tenantIdentifier, createDataSource(tenantIdentifier));
                }
                return dataSourceCache.get(tenantIdentifier).getConnection();
            }

            @Override
            protected void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
                connection.close();
            }
        };
    }
}
  1. 业务层使用:只需在请求头传入X-DB-Name,直接调用Repository即可,事务由Spring自动管理,完全符合Spring Data JPA的使用习惯。

方案对比

  • 动态路由数据源:实现简单,对现有代码侵入小,适合数据库结构一致的场景,但需要手动切换数据源标识。
  • Hibernate多租户:更规范,Spring Data JPA特性完全兼容,事务自动管理,但依赖Hibernate,配置稍复杂。

内容的提问来源于stack exchange,提问作者Murali

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 09:00:55