在Java开发中,单数据源早已无法满足复杂业务需求——主从读写分离、多租户数据隔离、分库分表……这些场景都需要多数据源支持。今天咱们就来聊聊Spring Boot中多数据源的静态配置动态切换实战,附完整代码和避坑指南!


一、为什么需要多数据源?

先明确场景,才能选对方案!

  • 读写分离:主库扛写操作(增删改),从库分担读压力(查询),提升系统吞吐量。
  • 多租户隔离:SaaS系统中,不同租户数据存独立数据库,避免数据泄露。
  • 分库分表:数据量暴增时,按业务或时间拆分到不同库/表,降低单库压力。
  • 异构数据库访问:同时连MySQL(关系型)、Redis(缓存)、MongoDB(文档型),满足多样化需求。

二、静态多数据源:固定数据源配置

适合数据源数量明确、无需动态切换的场景(比如固定的主从库)。

1. 配置文件定义多数据源

application.yml里直接声明两个数据源(主库primary和从库secondary),Spring Boot会自动加载这些配置。

spring:
  datasource:
    # 主数据源(写操作)
    primary:
      url: jdbc:mysql://localhost:3306/db_primary?useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:  # 连接池配置(可选)
        maximum-pool-size: 10  # 最大连接数
        connection-timeout: 30000  # 连接超时时间(ms)
    # 从数据源(读操作)
    secondary:
      url: jdbc:mysql://localhost:3307/db_secondary?useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        maximum-pool-size: 8

2. 手动创建数据源Bean

通过@Configuration@Bean显式创建两个DataSource,并用@Primary标记主数据源(避免Spring注入歧义)。

@Configuration
public class DataSourceConfig {

    // 主数据源(必须@Primary)
    @Primary
    @Bean("primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")  // 绑定yaml中primary前缀的配置
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();  // 直接用Builder创建
    }

    // 从数据源
    @Bean("secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

3. 配置MyBatis的SqlSessionFactory

如果用MyBatis,每个数据源需要独立的SqlSessionFactory,并通过@MapperScan指定Mapper接口的扫描路径。

主库配置类

@Configuration
@MapperScan(basePackages = "com.example.mapper.primary", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryMyBatisConfig {

    @Primary
    @Bean("primarySqlSessionFactory")
    public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);  // 绑定主数据源
        
        // 可选:指定Mapper XML文件位置(如果用了xml)
        sessionFactory.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources("classpath:mapper/primary/*.xml")
        );
        
        // 可选:设置MyBatis全局配置(如驼峰命名)
        sessionFactory.setConfiguration(mybatisConfig()); 
        
        return sessionFactory.getObject();
    }

    // 可选:MyBatis核心配置(如mapUnderscoreToCamelCase)
    @Bean
    public Configuration mybatisConfig() {
        Configuration configuration = new Configuration();
        configuration.setMapUnderscoreToCamelCase(true);  // 驼峰命名自动映射
        return configuration;
    }
}

从库配置类(类似主库,修改扫描路径和数据源引用即可):

@Configuration
@MapperScan(basePackages = "com.example.mapper.secondary", sqlSessionFactoryRef = "secondarySqlSessionFactory")
public class SecondaryMyBatisConfig {

    @Bean("secondarySqlSessionFactory")
    public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources("classpath:mapper/secondary/*.xml")
        );
        return sessionFactory.getObject();
    }
}

4. 配置事务管理器

每个数据源需要独立的事务管理器,否则事务会混乱!

@Configuration
public class TransactionManagerConfig {

    // 主库事务管理器
    @Primary
    @Bean("primaryTransactionManager")
    public PlatformTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // 从库事务管理器
    @Bean("secondaryTransactionManager")
    public PlatformTransactionManager secondaryTransactionManager(@Qualifier("secondaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

5. 在Service中指定数据源

通过@Qualifier注入对应的SqlSessionTemplate(或Mapper),或用@Transactional指定事务管理器。

@Service
public class UserService {

    // 主库Mapper(自动注入primarySqlSessionFactory对应的Bean)
    @Autowired
    private UserPrimaryMapper userPrimaryMapper; 

    // 从库Mapper
    @Autowired
    private UserSecondaryMapper userSecondaryMapper;

    // 操作主库(写)
    public User insertUser(User user) {
        userPrimaryMapper.insert(user);  // 插入主库
        return user;
    }

    // 操作从库(读)
    public User selectFromSecondary(Long id) {
        return userSecondaryMapper.selectById(id);  // 查询从库
    }
}

注意:如果用@Transactional,必须指定transactionManager属性!

@Transactional(transactionManager = "primaryTransactionManager")  // 主库事务
public void updateUser(User user) {
    userPrimaryMapper.updateById(user);
}

三、动态多数据源:运行时灵活切换

适合需要根据业务逻辑动态选择数据源的场景(比如多租户SaaS系统,根据租户ID切换数据库)。

1. 核心原理:AbstractRoutingDataSource

Spring提供的AbstractRoutingDataSource是抽象类,通过重写determineCurrentLookupKey()方法动态选择数据源。它内部维护了一个targetDataSources映射(数据源key → 具体DataSource),运行时根据当前线程的lookupKey选择数据源。

2. 实现步骤

步骤1:用ThreadLocal保存当前数据源key

线程隔离是关键!用ThreadLocal存储当前线程的数据源key,避免多线程干扰。

public class DataSourceContextHolder {
    // ThreadLocal保存当前数据源key(如"primary"/"tenant_123")
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    // 设置key
    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }

    // 获取key
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }

    // 清理key(防止内存泄漏!)
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}
步骤2:自定义动态数据源

继承AbstractRoutingDataSource,重写determineCurrentLookupKey(),从ThreadLocal获取当前key。

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // 从ThreadLocal获取当前数据源key
        return DataSourceContextHolder.getDataSourceKey();
    }
}
步骤3:配置多数据源与动态路由

在配置类中注册所有数据源,并将它们注入DynamicRoutingDataSourcetargetDataSources映射中。

@Configuration
public class DynamicDataSourceConfig {

    @Bean("dynamicRoutingDataSource")
    public DynamicRoutingDataSource dynamicRoutingDataSource(
            @Qualifier("primaryDataSource") DataSource primaryDataSource,
            @Qualifier("secondaryDataSource") DataSource secondaryDataSource,
            // 如果有其他数据源(如租户库),继续注入...
            @Qualifier("tenant1DataSource") DataSource tenant1DataSource) {

        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();

        // 设置默认数据源(未匹配key时使用)
        routingDataSource.setDefaultTargetDataSource(primaryDataSource);

        // 注册所有数据源(key是数据源标识,value是具体的DataSource)
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("primary", primaryDataSource);
        targetDataSources.put("secondary", secondaryDataSource);
        targetDataSources.put("tenant_123", tenant1DataSource);  // 示例租户库
        routingDataSource.setTargetDataSources(targetDataSources);

        return routingDataSource;
    }
}
步骤4:用AOP自动切换数据源

定义切面,在方法执行前根据注解切换数据源,执行后清理ThreadLocal

第一步:定义注解

@Target(ElementType.METHOD)  // 作用在方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留
public @interface TargetDataSource {
    String value();  // 数据源key(如"primary"/"tenant_123")
}

第二步:编写切面

@Aspect
@Component
public class DataSourceAspect {

    // 切点:标注了@TargetDataSource的方法
    @Pointcut("@annotation(com.example.annotation.TargetDataSource)")
    public void dataSourcePointcut() {}

    // 方法执行前切换数据源
    @Before("dataSourcePointcut()")
    public void before(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        TargetDataSource annotation = method.getAnnotation(TargetDataSource.class);
        if (annotation != null) {
            String dataSourceKey = annotation.value();
            DataSourceContextHolder.setDataSourceKey(dataSourceKey);  // 设置当前数据源key
        }
    }

    // 方法执行后清理ThreadLocal
    @After("dataSourcePointcut()")
    public void after() {
        DataSourceContextHolder.clear();  // 防止线程复用导致key残留
    }
}
步骤5:在Service中使用

在需要切换数据源的方法上添加@TargetDataSource注解,指定key即可。

@Service
public class TenantService {

    @Autowired
    private TenantMapper tenantMapper;  // 该Mapper绑定动态数据源

    // 操作租户123的数据库
    @TargetDataSource("tenant_123")
    public Tenant getTenantInfo(Long tenantId) {
        return tenantMapper.selectById(tenantId);
    }

    // 操作默认主库
    public User addUser(User user) {
        return userMapper.insert(user);  // 未标注注解,使用默认数据源(primary)
    }
}

四、避坑指南:常见问题与解决

1. 事务失效(最常见!)

现象:跨数据源操作时,@Transactional不生效,数据没提交。
原因

  • 未为@Transactional指定transactionManager(默认只关联一个数据源的事务管理器)。
  • 动态数据源切换在事务开启之后(事务开启时数据源已确定,后续切换无效)。

解决

  • 单数据源事务:显式指定transactionManager(如@Transactional(transactionManager = "primaryTransactionManager"))。
  • 跨数据源事务:必须用分布式事务(如Seata),本地事务无法保证原子性。

2. 线程安全问题(内存泄漏)

现象:线程复用时,数据源key错误(比如A线程设置了tenant_123,B线程复用该线程后仍用这个key)。
原因ThreadLocal未清理,导致线程池中的线程复用旧数据。

解决

  • 在切面的@After方法中调用DataSourceContextHolder.clear(),确保每次方法执行后清理。
  • 避免在@Async异步方法中使用动态数据源(除非手动传递ThreadLocal)。

3. MyBatis Mapper扫描冲突

现象:启动时报错No qualifying bean of type 'xxxMapper' available
原因:多个SqlSessionFactory的Mapper包路径重叠,Spring无法确定注入哪个。

解决

  • @MapperScan中明确指定sqlSessionFactoryRef(如sqlSessionFactoryRef = "primarySqlSessionFactory")。
  • 确保不同数据源的Mapper接口放在不同包路径(如com.example.mapper.primarycom.example.mapper.secondary)。

4. 性能问题(连接池耗尽)

现象:高并发下报Cannot get a connection from the pool
原因:动态数据源切换频繁,或连接池参数配置不合理(如最大连接数太小)。

解决

  • 调整连接池参数(如Hikari的maximum-pool-size设为CPU核心数*2+1)。
  • 减少不必要的动态切换(比如高频操作固定使用主库)。

五、总结

Spring Boot多数据源配置的核心是:

  • 静态多数据源:适合数据源固定的场景,通过@Primary@Qualifier区分。
  • 动态多数据源:适合运行时切换的场景,依赖AbstractRoutingDataSourceThreadLocal,配合AOP自动切换。

实际开发中,读写分离推荐静态配置(逻辑简单,性能稳定),多租户/分库分表推荐动态配置(灵活扩展)。无论哪种方案,都要注意事务边界和线程安全,避免踩坑!

如果觉得有用,记得点赞收藏,评论区留言讨论你的多数据源场景~ 😊

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐