Spring Boot如何实现多库同时连接及运行时动态新增数据库?
问题背景
我们正在将项目迁移至Spring Boot,这个Web应用需要连接2个预先已知的组织数据库,以及初始未知的客户数据库——客户数据库名称通过API传入,且API会被并发调用,因此需要支持动态连接不同数据库。我自己写了一段可运行的代码,但觉得实现方式不对,想请教三个问题:
- 不将
LocalContainerEntityManagerFactoryBean作为Bean使用是否可行? - 如何通过它创建Repository?
- 是否有更优的实现方案?
附当前代码:
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绑定,要动态绑定不同实例,可按以下方式实现:
- 手动实例化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);
- 手动管理事务:动态获取的
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的动态路由数据源
适合所有数据库结构一致的场景:
- 配置固定组织数据源:将两个已知的组织数据库注册为Spring Bean,配置对应的基础连接信息。
- 实现动态路由数据源:继承
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(); } }
- 初始化路由数据源:在配置类中把固定组织数据源加入路由,同时提供动态添加客户数据源的方法:
@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(); } }
- 业务层使用:在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多租户模式
适合典型多租户场景(每个客户一个独立数据库):
- 配置多租户策略:在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(); } }; } }
- 业务层使用:只需在请求头传入
X-DB-Name,直接调用Repository即可,事务由Spring自动管理,完全符合Spring Data JPA的使用习惯。
方案对比
- 动态路由数据源:实现简单,对现有代码侵入小,适合数据库结构一致的场景,但需要手动切换数据源标识。
- Hibernate多租户:更规范,Spring Data JPA特性完全兼容,事务自动管理,但依赖Hibernate,配置稍复杂。
内容的提问来源于stack exchange,提问作者Murali
相关产品推荐
相关产品推荐

