Spring框架下动态配置多数据源的实践指南

一、多数据源场景分类与需求分析

在分布式系统架构中,多数据源配置是解决跨库查询、读写分离、数据分片等场景的核心技术。根据业务需求差异,可划分为两类典型场景:

  1. 表级跨数据库场景
    当多个数据库存在完全相同的表结构时(如表名、字段定义一致),系统需根据业务规则动态选择数据源。典型应用场景包括:

    • 多租户系统:每个租户独立数据库,但表结构相同
    • 地理分区:按地域划分数据库,表结构保持一致
    • 灰度发布:新旧版本数据暂存不同数据库
  2. 非表级跨数据库场景
    当各数据源表结构完全不同时,系统需根据操作对象动态路由。常见场景包括:

    • 微服务架构:不同服务使用独立数据库
    • 异构数据整合:关系型数据库与NoSQL混合使用
    • 历史数据归档:活跃数据与历史数据分离存储

二、动态数据源核心实现原理

Spring框架通过AbstractRoutingDataSource抽象类实现动态数据源切换,其核心机制包含三个关键组件:

  1. 数据源路由器(DataSource Router)
    继承AbstractRoutingDataSource并重写determineCurrentLookupKey()方法,根据业务上下文返回数据源标识。示例实现:

    1. public class DynamicDataSource extends AbstractRoutingDataSource {
    2. @Override
    3. protected Object determineCurrentLookupKey() {
    4. return DataSourceContextHolder.getDataSourceType();
    5. }
    6. }
  2. 上下文管理器(Context Holder)
    使用ThreadLocal维护当前线程的数据源标识,确保线程安全。示例实现:

    1. public class DataSourceContextHolder {
    2. private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    3. public static void setDataSourceType(String dataSourceType) {
    4. contextHolder.set(dataSourceType);
    5. }
    6. public static String getDataSourceType() {
    7. return contextHolder.get();
    8. }
    9. public static void clearDataSourceType() {
    10. contextHolder.remove();
    11. }
    12. }
  3. 数据源配置中心
    通过YAML/Properties文件或配置中心动态管理数据源参数,支持运行时热更新。示例配置:

    1. spring:
    2. datasource:
    3. dynamic:
    4. primary: master # 默认数据源
    5. datasource:
    6. master:
    7. url: jdbc:mysql://localhost:3306/db1
    8. username: root
    9. password: 123456
    10. driver-class-name: com.mysql.cj.jdbc.Driver
    11. slave:
    12. url: jdbc:mysql://localhost:3306/db2
    13. username: root
    14. password: 123456
    15. driver-class-name: com.mysql.cj.jdbc.Driver

三、完整实现方案详解

1. 依赖引入与基础配置

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>mysql</groupId>
  7. <artifactId>mysql-connector-java</artifactId>
  8. </dependency>

2. 数据源初始化逻辑

  1. @Configuration
  2. public class DynamicDataSourceConfig {
  3. @Bean
  4. @ConfigurationProperties("spring.datasource.dynamic.datasource.master")
  5. public DataSource masterDataSource() {
  6. return DataSourceBuilder.create().build();
  7. }
  8. @Bean
  9. @ConfigurationProperties("spring.datasource.dynamic.datasource.slave")
  10. public DataSource slaveDataSource() {
  11. return DataSourceBuilder.create().build();
  12. }
  13. @Bean
  14. public DataSource dynamicDataSource() {
  15. Map<Object, Object> targetDataSources = new HashMap<>();
  16. targetDataSources.put("master", masterDataSource());
  17. targetDataSources.put("slave", slaveDataSource());
  18. DynamicDataSource dynamicDataSource = new DynamicDataSource();
  19. dynamicDataSource.setTargetDataSources(targetDataSources);
  20. dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
  21. return dynamicDataSource;
  22. }
  23. }

3. 动态切换实现方式

方式一:AOP切面实现

  1. @Aspect
  2. @Component
  3. public class DataSourceAspect {
  4. @Before("@annotation(targetDataSource)")
  5. public void beforeSwitchDataSource(JoinPoint point, TargetDataSource targetDataSource) {
  6. String dsName = targetDataSource.value();
  7. DataSourceContextHolder.setDataSourceType(dsName);
  8. }
  9. @After("@annotation(targetDataSource)")
  10. public void afterSwitchDataSource(JoinPoint point, TargetDataSource targetDataSource) {
  11. DataSourceContextHolder.clearDataSourceType();
  12. }
  13. }
  14. @Target({ElementType.METHOD, ElementType.TYPE})
  15. @Retention(RetentionPolicy.RUNTIME)
  16. public @interface TargetDataSource {
  17. String value();
  18. }

方式二:MyBatis拦截器实现

  1. @Intercepts({
  2. @Signature(type= Executor.class, method="update", args={MappedStatement.class, Object.class}),
  3. @Signature(type= Executor.class, method="query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
  4. })
  5. public class DynamicDataSourceInterceptor implements Interceptor {
  6. @Override
  7. public Object intercept(Invocation invocation) throws Throwable {
  8. MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
  9. String dataSource = getDataSourceFromAnnotation(mappedStatement);
  10. if (StringUtils.isNotBlank(dataSource)) {
  11. DataSourceContextHolder.setDataSourceType(dataSource);
  12. }
  13. try {
  14. return invocation.proceed();
  15. } finally {
  16. DataSourceContextHolder.clearDataSourceType();
  17. }
  18. }
  19. private String getDataSourceFromAnnotation(MappedStatement mappedStatement) {
  20. // 从SQL注释或XML配置中提取数据源标识
  21. return ...;
  22. }
  23. }

4. 事务管理注意事项

在动态数据源环境下,事务管理需特别注意:

  1. 事务传播行为:确保事务方法内不发生数据源切换
  2. 连接池配置:建议使用HikariCP等高性能连接池
  3. 异常处理:捕获CannotGetJdbcConnectionException等异常

四、最佳实践与性能优化

  1. 连接池优化

    1. spring:
    2. datasource:
    3. hikari:
    4. maximum-pool-size: 20
    5. minimum-idle: 5
    6. connection-timeout: 30000
    7. idle-timeout: 600000
  2. 监控告警集成
    通过Prometheus+Grafana监控各数据源连接数、活跃连接数等指标,设置阈值告警。

  3. 故障转移策略
    实现DataSourceHealthIndicator定期检查数据源可用性,自动剔除故障数据源。

  4. 读写分离优化
    结合注解实现自动读写分离:

    1. @TargetDataSource("master")
    2. public void writeOperation() { ... }
    3. @TargetDataSource("slave")
    4. public List<Data> readOperation() { ... }

五、常见问题解决方案

  1. 数据源切换失效
    检查是否忘记清除ThreadLocal上下文,或事务方法内发生切换

  2. 连接泄漏问题
    确保所有数据库操作都在try-with-resources块中执行

  3. 分布式事务挑战
    对于跨库事务,建议采用Seata等分布式事务框架

  4. 动态刷新配置
    集成配置中心实现数据源参数动态更新,无需重启应用

通过上述技术方案,开发者可构建高可用、易扩展的动态数据源管理系统。实际项目中,建议结合具体业务场景选择合适的实现方式,并建立完善的监控运维体系确保系统稳定性。