告别硬编码!SpringBoot项目实战:基于Header动态切换MyBatis-Plus多数据源
在微服务架构和SaaS平台开发中,多租户数据隔离是一个常见需求。传统做法往往需要在代码中硬编码数据源配置,这不仅降低了系统的灵活性,也为后续维护埋下了隐患。本文将介绍如何利用MyBatis-Plus和dynamic-datasource组件,通过HTTP请求头实现优雅的动态数据源切换方案。
1. 多数据源架构设计原理
1.1 核心组件解析
MyBatis-Plus生态中的dynamic-datasource组件提供了多数据源管理的核心能力,其设计基于两个关键类:
- DynamicRoutingDataSource:继承自Spring的AbstractRoutingDataSource,负责实际的数据源路由决策
- DynamicDataSourceContextHolder:基于ThreadLocal的上下文保持器,存储当前线程使用的数据源标识
// 典型的数据源路由决策实现 public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.peek(); } }1.2 线程安全设计考量
在多线程环境下,数据源切换必须保证线程隔离。dynamic-datasource采用双端队列(Deque)结构存储数据源标识,支持嵌套调用场景:
| 方法名 | 作用 | 线程安全保证 |
|---|---|---|
| push() | 压入数据源标识 | ThreadLocal存储 |
| peek() | 获取当前数据源 | 不修改栈结构 |
| poll() | 弹出数据源标识 | 自动清理资源 |
这种设计确保了:
- 每个线程独立维护自己的数据源上下文
- 方法调用栈中的数据源切换互不干扰
- 避免内存泄漏风险
2. 基于Header的动态切换实现
2.1 过滤器(Filter)实现方案
过滤器是处理HTTP请求的第一道关卡,适合实现全局性的数据源切换逻辑。以下是完整的Filter实现示例:
@WebFilter(urlPatterns = "/*") public class DataSourceFilter implements Filter { private static final String DS_HEADER = "X-Tenant-ID"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String tenantId = httpRequest.getHeader(DS_HEADER); if (StringUtils.isNotBlank(tenantId)) { DynamicDataSourceContextHolder.push(tenantId); } try { chain.doFilter(request, response); } finally { DynamicDataSourceContextHolder.clear(); } } }关键点说明:
- 从
X-Tenant-ID请求头获取租户标识 - 使用try-finally确保数据源上下文始终被清理
- 支持通配符URL模式,覆盖所有请求路径
2.2 拦截器(Interceptor)实现方案
相比Filter,拦截器可以获取更多Spring上下文信息,适合需要依赖注入的场景:
public class DataSourceInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String dataSourceKey = resolveDataSourceKey(request); if (dataSourceKey != null) { DynamicDataSourceContextHolder.push(dataSourceKey); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { DynamicDataSourceContextHolder.clear(); } private String resolveDataSourceKey(HttpServletRequest request) { // 可扩展为从cookie、JWT等获取标识 return request.getHeader("X-Data-Source"); } }注册拦截器配置:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new DataSourceInterceptor()) .addPathPatterns("/api/**"); } }3. 生产环境最佳实践
3.1 数据源健康检查机制
动态数据源环境下,建议实现定期健康检查:
@Scheduled(fixedRate = 30000) public void checkDataSourceHealth() { Map<String, DataSource> dataSources = dynamicRoutingDataSource.getDataSources(); dataSources.forEach((key, ds) -> { try (Connection conn = ds.getConnection()) { conn.createStatement().execute("SELECT 1"); } catch (SQLException e) { logger.error("DataSource {} health check failed", key, e); // 触发告警或自动下线 } }); }3.2 多级回退策略
设计健壮的数据源切换策略应考虑以下优先级:
- 请求头指定:最高优先级,如
X-Data-Source: tenant_a - 用户会话信息:从认证信息中提取租户标识
- 默认数据源:配置的primary数据源
public String determineDataSourceKey(HttpServletRequest request) { // 1. 检查请求头 String headerKey = request.getHeader("X-Data-Source"); if (isValidDataSource(headerKey)) { return headerKey; } // 2. 检查JWT声明 String jwtTenant = resolveFromJWT(request); if (isValidDataSource(jwtTenant)) { return jwtTenant; } // 3. 返回默认数据源 return "master"; }4. 性能优化与问题排查
4.1 连接池配置建议
不同数据源应独立配置连接池参数(以Druid为例):
| 参数 | 主库 | 从库 | 租户库 |
|---|---|---|---|
| initialSize | 5 | 3 | 2 |
| maxActive | 20 | 15 | 10 |
| minIdle | 5 | 3 | 2 |
| maxWait | 3000 | 5000 | 5000 |
spring: datasource: druid: master: initial-size: 5 max-active: 20 tenant_a: initial-size: 2 max-active: 104.2 常见问题排查指南
问题1:数据源未正确切换
- 检查请求头是否被正确传递
- 确认Filter/Interceptor执行顺序
- 调试
determineCurrentLookupKey()方法
问题2:连接泄漏
- 确保每次push()都有对应的clear()
- 检查事务边界是否正确
- 使用连接池监控工具
问题3:性能下降
- 检查连接池配置是否合理
- 考虑增加数据源缓存
- 评估是否需要读写分离
5. 进阶应用场景
5.1 多租户SaaS平台实现
典型的多租户数据隔离方案对比:
| 方案 | 隔离级别 | 优点 | 缺点 |
|---|---|---|---|
| 独立数据库 | 数据库级 | 完全隔离 | 成本高 |
| 共享库独立Schema | Schema级 | 较好隔离 | 需要DB支持 |
| 共享表租户ID | 行级 | 成本低 | 改造量大 |
基于Header的动态切换最适合前两种方案。实现时可结合租户注册中心:
public class TenantDataSourceResolver { @Autowired private TenantRegistry registry; public String resolve(HttpServletRequest request) { String tenantId = request.getHeader("X-Tenant-ID"); TenantInfo tenant = registry.getTenant(tenantId); return tenant != null ? tenant.getDataSourceKey() : "master"; } }5.2 灰度发布支持
通过数据源切换实现数据库灰度发布:
- 在Header中指定版本标记:
X-Data-Version: v2 - 路由到对应版本的数据源
- 新旧版本数据源可配置双写
@Aspect @Component public class DataSourceAspect { @Around("@annotation(dataSource)") public Object around(ProceedingJoinPoint pjp, DataSource dataSource) throws Throwable { String version = RequestContextHolder.getRequestAttributes() .getHeader("X-Data-Version"); String originalKey = DynamicDataSourceContextHolder.peek(); try { if ("v2".equals(version)) { DynamicDataSourceContextHolder.push(originalKey + "_v2"); } return pjp.proceed(); } finally { DynamicDataSourceContextHolder.clear(); } } }在实际项目中,这种基于请求头的动态数据源切换方案已经帮助多个团队实现了灵活的多租户架构。特别是在SaaS化改造过程中,无需修改业务代码就能支持新租户的数据库隔离需求,大大提高了系统的可扩展性。