Java如何实现多数据源切换

在Java中实现多数据源切换通常用于需要动态切换不同数据库(如主从库、分库分表、多租户等)的场景。以下是常见的实现方案,基于Spring/Spring Boot框架:


1. 方案概览

方案 适用场景 优点 缺点
AbstractRoutingDataSource 简单动态切换(如主从库) 轻量级,Spring原生支持 需手动管理连接,不支持复杂事务
MyBatis插件 MyBatis项目,需拦截SQL选择数据源 灵活,与ORM深度集成 需熟悉MyBatis拦截器机制
ShardingSphere 分库分表、读写分离等复杂场景 功能强大,支持分布式事务 学习成本较高
JPA+Hibernate 使用JPA的项目 与JPA无缝集成 配置复杂

2. 具体实现(以AbstractRoutingDataSource为例)

步骤1:定义数据源枚举与上下文持有器

// 数据源类型枚举
public enum DataSourceType {
    MASTER, SLAVE
}

// 使用ThreadLocal保存当前线程的数据源标识
public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<DataSourceType> CONTEXT = new ThreadLocal<>();

    public static void setDataSourceType(DataSourceType type) {
        CONTEXT.set(type);
    }

    public static DataSourceType getDataSourceType() {
        return CONTEXT.get() == null ? DataSourceType.MASTER : CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

步骤2:继承AbstractRoutingDataSource实现动态路由

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

步骤3:配置多数据源Bean

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource dynamicDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER, masterDataSource());
        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource());

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        return dynamicDataSource;
    }
}

步骤4:配置application.yml

spring:
  datasource:
    master:
      url: jdbc:mysql://localhost:3306/master_db
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave:
      url: jdbc:mysql://localhost:3306/slave_db
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver

步骤5:使用AOP或注解切换数据源

  • 自定义注解

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DataSource {
        DataSourceType value() default DataSourceType.MASTER;
    }
    
  • AOP切面

    @Aspect
    @Component
    public class DataSourceAspect {
        @Before("@annotation(dataSource)")
        public void beforeSwitchDataSource(DataSource dataSource) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value());
        }
    
        @After("@annotation(dataSource)")
        public void afterClearDataSource(DataSource dataSource) {
            DynamicDataSourceContextHolder.clear();
        }
    }
    
  • 使用注解切换

    @Service
    public class UserService {
        @DataSource(DataSourceType.MASTER)
        public void addUser(User user) {
            // 使用主库
        }
    
        @DataSource(DataSourceType.SLAVE)
        public User getUserById(Long id) {
            // 使用从库
            return userMapper.selectById(id);
        }
    }
    

3. 高级场景优化

事务管理

  • 问题:直接使用AbstractRoutingDataSource时,事务内无法切换数据源(因为事务会绑定初始数据源)。
  • 解决方案
    • 使用@Transactional(propagation = Propagation.REQUIRES_NEW)开启新事务。
    • 结合ShardingSphere或Atomikos实现分布式事务。

多租户支持

  • DynamicDataSourceContextHolder中存储租户ID,动态选择对应租户的数据源:

    public class TenantDataSourceAspect {
        @Before("execution(* com.example..*.*(..))")
        public void setTenantDataSource(JoinPoint joinPoint) {
            String tenantId = TenantContext.getCurrentTenant();
            DynamicDataSourceContextHolder.setDataSourceType(tenantId);
        }
    }
    

4. 其他方案对比

MyBatis插件实现

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MyBatisDataSourceInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        if (ms.getSqlCommandType() == SqlCommandType.SELECT) {
            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
        } else {
            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
        }
        return invocation.proceed();
    }
}

ShardingSphere配置

spring:
  shardingsphere:
    datasource:
      names: master,slave
      master:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://localhost:3306/master_db
        username: root
        password: 123456
      slave:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://localhost:3306/slave_db
        username: root
        password: 123456
    masterslave:
      load-balance-algorithm-type: round_robin
      name: ms_ha
      master-data-source-name: master
      slave-data-source-names: slave

5. 总结

  • 简单场景AbstractRoutingDataSource + 注解/AOP。
  • 复杂场景:ShardingSphere(支持分库分表、读写分离、分布式事务)。
  • 关键点
    • 使用ThreadLocal保证线程安全。
    • 注意事务内数据源切换的局限性。
    • 生产环境建议配合连接池(如HikariCP)使用。
posted @ 2025-08-31 15:08  little_lunatic  阅读(170)  评论(0)    收藏  举报