Spring多租户数据库路由异常:登录时无法切换租户数据源
1. 检查AbstractRoutingDataSource核心实现
你的TenantManager继承AbstractRoutingDataSource后,必须确保determineCurrentLookupKey()方法正确从线程上下文取租户ID,而不是从错误的来源获取。比如:
@Override protected Object determineCurrentLookupKey() { // 这里必须和你setCurrentTenant()存入的ThreadLocal是同一个 return TenantContextHolder.getCurrentTenantId(); }
如果存和取的ThreadLocal不是同一个,哪怕你打印currentTenant正确,路由时也会拿不到值,直接走默认数据源。
2. 排查线程上下文传递问题
如果你的employeeRepository.findByEmail()是在异步线程里执行(比如加了@Async),ThreadLocal里的租户ID不会自动传递到异步线程,导致查询时上下文丢失。解决办法:
- 用
TaskDecorator复制ThreadLocal内容到异步线程:
@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setTaskDecorator(runnable -> { // 先捕获当前线程的租户ID String tenantId = TenantContextHolder.getCurrentTenantId(); return () -> { try { // 在异步线程中设置租户ID TenantContextHolder.setCurrentTenantId(tenantId); runnable.run(); } finally { // 执行完清理上下文 TenantContextHolder.clear(); } }; }); executor.initialize(); return executor; } }
3. 调整租户ID的设置时机
如果你的setCurrentTenant()是在业务方法中临时设置,可能会遇到Spring Security在请求结束后自动清理SecurityContext,导致Repository查询时上下文已失效。正确的做法是在请求入口就解析租户ID并存入ThreadLocal:
@Component public class TenantFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从JWT中解析租户ID(这里替换成你实际的解析逻辑) String tenantId = extractTenantIdFromJwt(request); if (tenantId != null) { TenantContextHolder.setCurrentTenantId(tenantId); } try { filterChain.doFilter(request, response); } finally { // 请求结束后清理上下文,避免内存泄漏 TenantContextHolder.clear(); } } }
这样整个请求生命周期内ThreadLocal都持有租户ID,Repository查询时就能正确路由。
4. 验证数据源配置的正确性
确保你在配置类中正确初始化了TenantManager,并且租户数据源的key和你传入的tenant_id完全匹配(大小写、格式都不能错):
@Bean public DataSource dataSource() { TenantManager routingDataSource = new TenantManager(); Map<Object, Object> targetDataSources = new HashMap<>(); // 注意key要和JWT里的tenant_id完全一致 targetDataSources.put("tenant_001", dataSourceForTenant001()); targetDataSources.put("tenant_002", dataSourceForTenant002()); routingDataSource.setTargetDataSources(targetDataSources); // 默认数据源没有employee表,必须确保租户ID存在时不会走到这里 routingDataSource.setDefaultTargetDataSource(defaultDataSource()); routingDataSource.afterPropertiesSet(); return routingDataSource; }
如果key不匹配,路由逻辑会找不到对应数据源,直接走默认库。
5. 检查Repository的数据源绑定
确认你的EmployeeRepository没有被硬编码绑定到某个特定数据源(比如用@Qualifier("defaultDataSource")指定了默认库),如果有这类注解,会直接覆盖路由逻辑,必须移除。
6. 开启日志定位问题
在application.yml中开启SQL相关日志,直接看执行查询时用的是哪个数据源:
logging: level: org.springframework.jdbc.datasource: DEBUG org.hibernate.SQL: DEBUG
通过日志可以明确看到数据源的URL,确认是不是真的走到了默认库,帮助快速定位问题。
内容的提问来源于stack exchange,提问作者Omotoso Iyanu

