Spring Boot路由数据源无法正常工作问题求助
哥们,我之前在做类似的多租户Spring Boot项目时,也踩过AbstractRoutingDataSource失效的坑,结合你用到的Atomikos、Hibernate/JPA这些技术栈,大概率是几个关键配置没到位,咱们一步步捋:
1. 先确保TenantContextHolder的线程安全性
TenantContextHolder是数据源路由的核心依据,要是它不是线程安全的,多请求下租户ID肯定串线,路由直接失效。必须用ThreadLocal来存租户ID,还要记得请求结束后清理:
public class TenantContextHolder { private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>(); public static void setCurrentTenantId(String tenantId) { CURRENT_TENANT.set(tenantId); } public static String getCurrentTenantId() { return CURRENT_TENANT.get(); } // 关键!请求结束后清理,避免内存泄漏和线程复用导致的串号 public static void clear() { CURRENT_TENANT.remove(); } }
然后加个拦截器,在请求进来时设置租户ID(比如从请求头X-Tenant-ID拿),结束后清理:
public class TenantInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String tenantId = request.getHeader("X-Tenant-ID"); TenantContextHolder.setCurrentTenantId(tenantId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { TenantContextHolder.clear(); } }
别忘了把这个拦截器注册到Spring MVC配置里。
2. 修复动态数据源构建的坑
你在determineCurrentLookupKey()里动态创建数据源并存到resolvedDataSourceMap,这里有两个致命问题:
- Map必须线程安全:要用
ConcurrentHashMap,不然多线程下会出现数据源重复创建或丢失的情况; - 必须用Atomikos兼容的XA数据源:因为你用了分布式事务,普通DataSource和Atomikos不兼容,直接导致路由或事务失效。Postgres的XA数据源示例:
private DataSource constructDataSource(Setting setting, String endpoint, String databaseName) { AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setUniqueResourceName("tenant-" + databaseName); // 每个租户的数据源名要唯一 xaDataSource.setXaDataSourceClassName("org.postgresql.xa.PGXADataSource"); Properties props = new Properties(); props.setProperty("user", setting.getDbUser()); props.setProperty("password", setting.getDbPassword()); props.setProperty("serverName", endpoint); props.setProperty("databaseName", databaseName); xaDataSource.setXaProperties(props); // 配置连接池参数 xaDataSource.setMinPoolSize(5); xaDataSource.setMaxPoolSize(20); return xaDataSource; }
另外要确保determineCurrentLookupKey()返回的租户ID和resolvedDataSourceMap的key完全匹配,别出现大小写、格式不一致的情况。
3. 让Hibernate/JPA乖乖配合RoutingDataSource
Hibernate很容易绕过RoutingDataSource直接拿连接,必须配置到位:
- 在
application.properties里,指定JPA用你的RoutingDataSource,同时禁用Hibernate自带的连接池(交给Atomikos管理):
spring.jpa.properties.hibernate.connection.provider_class=org.hibernate.connection.DatasourceConnectionProvider spring.jpa.properties.hibernate.transaction.jta.platform=com.atomikos.icatch.jta.hibernate4.AtomikosPlatform
- 如果是Spring Boot 2.x+,自定义
EntityManagerFactory时要明确传入你的RoutingDataSource:
@Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource routingDataSource) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(routingDataSource); em.setPackagesToScan("com.yourpackage.model"); // 替换成你的实体类包 HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); Properties properties = new Properties(); properties.put("hibernate.transaction.jta.platform", "com.atomikos.icatch.jta.hibernate4.AtomikosPlatform"); properties.put("hibernate.connection.provider_class", "org.hibernate.connection.DatasourceConnectionProvider"); em.setJpaProperties(properties); return em; }
4. 事务管理的额外注意点
Atomikos作为分布式事务管理器,要确保所有动态创建的数据源都是XA兼容的。另外,别在determineCurrentLookupKey()里做耗时操作(比如去默认库查租户配置)——这个方法每次拿连接都会调用,不仅慢,还可能打乱事务上下文。建议提前把租户的数据源配置缓存起来,比如首次请求时加载,或者应用启动时预加载。
5. 调试技巧
在determineCurrentLookupKey()里加日志,打印当前线程ID和租户ID,确认每次调用都能拿到正确的值:
@Override protected Object determineCurrentLookupKey() { String currentTenantId = TenantContextHolder.getCurrentTenantId(); log.info("当前线程ID: {}, 租户ID: {}", Thread.currentThread().getId(), currentTenantId); // 后续逻辑... }
同时检查RoutingDataSource的targetDataSources是否正确关联了你的resolvedDataSourceMap,如果是自定义Map,可能需要重写getResolvedDataSources()方法让Spring能读到。
内容的提问来源于stack exchange,提问作者Thanh Dong

