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

如何在Spring Boot中实现基于API请求的动态数据库连接?

Spring Boot 动态数据库连接实现方案

针对你需要根据API请求动态切换数据库的需求,这里给出一套落地性强的实现方案,适配从多Schema到多数据库的租户架构升级:

核心思路

  1. 从集中式配置库读取租户对应的数据库连接信息
  2. 用ThreadLocal存储当前请求的租户标识,确保请求线程内数据源一致
  3. 基于AbstractRoutingDataSource实现动态数据源路由,自动切换连接
  4. 缓存已创建的数据源,避免重复初始化开销

具体实现步骤

1. 定义租户数据库配置实体

先封装数据库连接所需的核心参数:

@Data
public class TenantDbConfig {
    private String dbUrl;
    private String username;
    private String password;
    private String driverClassName = "com.mysql.cj.jdbc.Driver";
}

2. 实现集中式配置库的查询服务

专门写一个服务类,负责从你的集中式配置库中获取租户对应的数据库配置:

@Service
public class TenantConfigService {
    @Autowired
    private JdbcTemplate configJdbcTemplate; // 这个模板绑定的是集中式配置库的数据源

    public TenantDbConfig getTenantDbConfig(String tenantId) {
        String sql = "SELECT db_url, username, password FROM tenant_db_config WHERE tenant_id = ?";
        return configJdbcTemplate.queryForObject(sql, new Object[]{tenantId},
                (rs, rowNum) -> {
                    TenantDbConfig config = new TenantDbConfig();
                    config.setDbUrl(rs.getString("db_url"));
                    config.setUsername(rs.getString("username"));
                    config.setPassword(rs.getString("password"));
                    return config;
                });
    }
}

3. 动态数据源管理类

用线程安全的Map缓存已创建的数据源,同时通过ThreadLocal管理当前请求的租户标识:

@Component
public class DynamicDataSourceManager {
    private final Map<String, DataSource> dataSourceCache = new ConcurrentHashMap<>();
    private final ThreadLocal<String> currentTenantId = new ThreadLocal<>();
    
    @Autowired
    private TenantConfigService tenantConfigService;
    @Autowired
    private DataSource defaultDataSource; // 默认的通用数据库连接

    public DataSource getCurrentDataSource() {
        String tenantId = currentTenantId.get();
        // 没有指定租户则返回默认数据源
        if (tenantId == null) {
            return defaultDataSource;
        }
        // 缓存中不存在则创建新的数据源
        return dataSourceCache.computeIfAbsent(tenantId, this::createTenantDataSource);
    }

    private DataSource createTenantDataSource(String tenantId) {
        TenantDbConfig config = tenantConfigService.getTenantDbConfig(tenantId);
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(config.getDbUrl());
        dataSource.setUsername(config.getUsername());
        dataSource.setPassword(config.getPassword());
        dataSource.setDriverClassName(config.getDriverClassName());
        // 根据业务需求配置连接池参数
        dataSource.setMaximumPoolSize(10);
        dataSource.setConnectionTimeout(30000);
        return dataSource;
    }

    public void setCurrentTenantId(String tenantId) {
        currentTenantId.set(tenantId);
    }

    public void clearCurrentTenantId() {
        currentTenantId.remove();
    }
}

4. 自定义动态路由数据源

继承Spring的AbstractRoutingDataSource,重写数据源路由逻辑:

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Autowired
    private DynamicDataSourceManager dataSourceManager;

    @Override
    protected DataSource determineTargetDataSource() {
        // 直接返回当前请求对应的数据源
        return dataSourceManager.getCurrentDataSource();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        // 此方法为父类要求实现,返回租户ID即可(父类内部不会实际使用这个值,因为我们重写了determineTargetDataSource)
        return dataSourceManager.getCurrentTenantId().get();
    }
}

然后在配置类中注册这个动态数据源,设置默认数据源并标记为@Primary:

@Configuration
public class DataSourceConfig {
    @Autowired
    private Environment env;

    @Bean
    public DataSource defaultDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(env.getProperty("spring.datasource.url"));
        dataSource.setUsername(env.getProperty("spring.datasource.username"));
        dataSource.setPassword(env.getProperty("spring.datasource.password"));
        dataSource.setDriverClassName(env.getProperty("spring.datasource.driver-class-name"));
        return dataSource;
    }

    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setDefaultTargetDataSource(defaultDataSource());
        return dataSource;
    }

    // 如果使用JPA,需要配置EntityManagerFactory绑定动态数据源
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dynamicDataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dynamicDataSource);
        em.setPackagesToScan("com.yourproject.entity"); // 替换成你的实体类包路径
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        
        Properties jpaProperties = new Properties();
        jpaProperties.setProperty("hibernate.hbm2ddl.auto", env.getProperty("spring.jpa.hibernate.ddl-auto"));
        jpaProperties.setProperty("hibernate.dialect", env.getProperty("spring.jpa.properties.hibernate.dialect"));
        em.setJpaProperties(jpaProperties);
        return em;
    }

    // 配置JdbcTemplate绑定动态数据源(如果使用JDBC)
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dynamicDataSource) {
        return new JdbcTemplate(dynamicDataSource);
    }
}

5. 请求拦截器设置租户标识

通过拦截器从请求中获取租户ID(比如请求头X-Tenant-Id),并设置到DynamicDataSourceManager中,请求结束后清理:

@Component
public class TenantInterceptor implements HandlerInterceptor {
    @Autowired
    private DynamicDataSourceManager dataSourceManager;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求头获取租户ID,也可以从请求参数、路径变量中获取,根据你的业务规则调整
        String tenantId = request.getHeader("X-Tenant-Id");
        if (tenantId != null && !tenantId.isBlank()) {
            dataSourceManager.setCurrentTenantId(tenantId);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清理ThreadLocal,避免内存泄漏
        dataSourceManager.clearCurrentTenantId();
    }
}

最后注册拦截器,对所有请求生效:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private TenantInterceptor tenantInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tenantInterceptor)
                .addPathPatterns("/**");
    }
}

关键注意事项

  • 数据源缓存:用ConcurrentHashMap缓存已创建的数据源,避免重复初始化连接池,提升性能
  • 连接池配置:每个动态数据源的连接池参数要根据租户规模合理配置,防止资源耗尽
  • 异常处理:如果集中式配置库查询不到租户配置,建议返回默认数据源或抛出明确的业务异常
  • 线程安全:务必在请求结束后清理ThreadLocal,否则会导致内存泄漏
  • 事务适配:Spring的@Transactional会自动适配动态数据源,无需额外配置;但跨数据源事务需要引入分布式事务框架(如Seata)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 00:17:33