java 数据库连接池配置租户路由的核心做法是:使用支持多数据源的路由中间件(如 shardingsphere-jdbc 或自定义 dynamicdatasource),将租户标识绑定到当前线程,并在获取连接时根据该标识动态切换目标数据源。具体实现需定义一个包含所有租户数据源的 map,通过 threadlocal 存储当前租户 id,在拦截器或过滤器中设置租户上下文,最终由路由逻辑决定从哪个具体的 datasource 获取物理连接。

如何基于 ThreadLocal 实现动态数据源切换
通过继承 AbstractRoutingDataSource 并重写 determineCurrentLookupKey 方法,可以实现在运行时动态选择数据源。关键在于利用 ThreadLocal 存储当前请求的租户 ID,确保多线程环境下上下文隔离。当应用发起数据库操作时,Spring 框架会调用 determineCurrentLookupKey 获取当前租户键,进而从预注册的数据源映射表中找到对应的物理连接池实例。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getTenantId();
}
}
@Configuration
public class DataSourceConfig {
@Bean
public DataSource routingDataSource(Map<string datasource> targetDataSources) {
TenantRoutingDataSource routingDataSource = new TenantRoutingDataSource();
routingDataSource.setTargetDataSources(new HashMap(targetDataSources));
// 必须设置默认数据源,防止租户ID为空时抛出异常
routingDataSource.setDefaultTargetDataSource(targetDataSources.get("default"));
return routingDataSource;
}
}</string>
上述代码展示了核心路由类的实现。TenantContext 是一个简单的工具类,内部持有 ThreadLocal
如何配置多租户数据源映射与初始化
在 Spring Boot 应用中,通常通过配置文件或 Java Config 定义多个独立的 DataSource Bean,并将它们注入到路由数据源中。每个租户对应一个独立的 JDBC URL、用户名和密码,形成逻辑上的数据隔离。初始化阶段需要遍历所有租户配置,创建对应的 HikariCP 或 Druid 连接池实例,并放入 Map 中供路由层使用。

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class MultiTenantDataSourceConfig {
@Value("${tenant.db.default.url}")
private String defaultUrl;
@Value("${tenant.db.default.username}")
private String defaultUser;
@Value("${tenant.db.default.password}")
private String defaultPass;
@Value("${tenant.db.tenantA.url}")
private String tenantAUrl;
@Value("${tenant.db.tenantA.username}")
private String tenantAUser;
@Value("${tenant.db.tenantA.password}")
private String tenantAPass;
@Bean(name = "dataSource")
@Primary
public DataSource primaryDataSource() {
Map<object object> targetDataSources = new HashMap();
// 配置默认数据源
HikariDataSource defaultDs = createHikari(defaultUrl, defaultUser, defaultPass);
targetDataSources.put("default", defaultDs);
// 配置租户 A 数据源
HikariDataSource tenantADs = createHikari(tenantAUrl, tenantAUser, tenantAPass);
targetDataSources.put("tenantA", tenantADs);
return new TenantRoutingDataSourceWrapper(targetDataSources, defaultDs);
}
private HikariDataSource createHikari(String url, String user, String pass) {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl(url);
ds.setUsername(user);
ds.setPassword(pass);
ds.setMaximumPoolSize(10);
return ds;
}
}</object>
此配置类演示了如何手动构建多数据源映射。createHikari 方法封装了连接池的基本参数设置。实际生产中,建议将租户配置抽取为列表结构,通过循环动态生成 DataSource Bean,以支持无限扩展租户数量。TenantRoutingDataSourceWrapper 是前文提到的路由类的包装,它接收已初始化的 targetDataSources Map 并完成设置。这种显式配置方式便于调试和监控每个租户的连接池状态。
当租户 ID 未设置时连接池的行为是什么
如果请求链路中未正确设置租户 ID,AbstractRoutingDataSource 的 determineCurrentLookupKey 方法将返回 null。此时,连接池会强制回退到通过 setDefaultTargetDataSource 配置的默认数据源。这意味着所有未标记租户的请求都将共享同一个物理连接池实例,可能导致不同租户数据混淆。因此,必须在网关或拦截器层强制校验租户 ID 的存在性,避免静默降级带来的数据安全风险。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











