如何在Spring Boot中实现基于API请求的动态数据库连接?
Spring Boot 动态数据库连接实现方案
针对你需要根据API请求动态切换数据库的需求,这里给出一套落地性强的实现方案,适配从多Schema到多数据库的租户架构升级:
核心思路
- 从集中式配置库读取租户对应的数据库连接信息
- 用
ThreadLocal存储当前请求的租户标识,确保请求线程内数据源一致 - 基于
AbstractRoutingDataSource实现动态数据源路由,自动切换连接 - 缓存已创建的数据源,避免重复初始化开销
具体实现步骤
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
相关产品推荐
相关产品推荐

