人人都会AI编程

13.6 多数据源配置与动态数据源实现

更新时间:2026-07-11

当单一数据库不再能满足业务需求时,拆分数据到不同库或连接不同业务系统就成了常态。Spring 提供了灵活的多数据源支持,从“静态绑定多个独立 DataSource”到“运行时动态切换数据源”,都可以通过简洁的扩展点实现。

13.6.1 为什么要配置多数据源

常见场景包括:

  • 读写分离:主库处理写入,从库处理查询,提升并发能力。
  • 垂直分库:订单库、用户库、商品库分离部署,降低耦合。
  • 多租户:不同租户使用不同数据库,实现物理隔离。
  • 对接外部系统:连接第三方只读库或同步数据。

在这些场景下,应用需要同时持有多个数据源,并能根据业务上下文选择正确的连接。

13.6.2 静态多数据源配置

最基本的做法是分别创建多个 DataSource Bean,并显式为不同持久层组件指定数据源。

1. 自定义多数据源并绑定到不同 JdbcTemplate

@Configuration
public class MultiDataSourceConfig {

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

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

    @Bean
    public JdbcTemplate primaryJdbcTemplate(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate(
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

配置文件:

spring:
  datasource:
    primary:
      url: jdbc:mysql://localhost:3306/primary_db
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    secondary:
      url: jdbc:mysql://localhost:3306/secondary_db
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

使用时直接注入对应 JdbcTemplate:

@Service
public class OrderService {
    private final JdbcTemplate primaryJdbc;
    private final JdbcTemplate secondaryJdbc;

    public OrderService(@Qualifier("primaryJdbcTemplate") JdbcTemplate primaryJdbc,
                        @Qualifier("secondaryJdbcTemplate") JdbcTemplate secondaryJdbc) {
        this.primaryJdbc = primaryJdbc;
        this.secondaryJdbc = secondaryJdbc;
    }
}

2. 结合 Spring Data JPA 的多数据源

需要为每个数据源提供独立的 EntityManagerFactory 和事务管理器。以两个 JPA 数据源为例:

@Configuration
@EnableTransactionManagement
public class JpaMultiDataSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "primaryEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean primaryEmf(
            EntityManagerFactoryBuilder builder,
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return builder.dataSource(dataSource)
                .packages("com.example.entity.primary")
                .persistenceUnit("primary")
                .build();
    }

    @Bean(name = "secondaryEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean secondaryEmf(
            EntityManagerFactoryBuilder builder,
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        return builder.dataSource(dataSource)
                .packages("com.example.entity.secondary")
                .persistenceUnit("secondary")
                .build();
    }

    @Primary
    @Bean(name = "primaryTransactionManager")
    public PlatformTransactionManager primaryTx(
            @Qualifier("primaryEntityManagerFactory") EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }

    @Bean(name = "secondaryTransactionManager")
    public PlatformTransactionManager secondaryTx(
            @Qualifier("secondaryEntityManagerFactory") EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

随后在 Repository 上通过 @EnableJpaRepositories 指定实体、事务管理器等,将不同的包绑定到不同的数据源。

静态方式简单直接,但当数据源数量动态变化或切换逻辑复杂时,硬编码的 Bean 定义就力不从心了。

13.6.3 动态数据源实现(AbstractRoutingDataSource)

Spring 提供了一个抽象类 AbstractRoutingDataSource,它能够根据一个运行时上下文动态决定使用哪个数据源,完美支持读写分离、多租户等场景。

核心原理AbstractRoutingDataSource 在每次获取连接时调用 determineCurrentLookupKey() 方法,返回一个 key(通常是字符串),然后使用这个 key 从预先配置的 Map 中选择对应的 DataSource。

实现步骤

1. 创建数据源上下文持有者

利用 ThreadLocal 保存当前线程的数据源标识,确保线程安全。

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    public static void clearDataSourceKey() {
        contextHolder.remove();
    }
}

2. 继承 AbstractRoutingDataSource 实现动态路由

@Component("dynamicDataSource")
@Primary
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }
}

3. 配置主从数据源并注入到 DynamicRoutingDataSource

@Configuration
public class DynamicDataSourceConfig {

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

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

    @Bean
    @Primary
    public DataSource dataSource(
            @Qualifier("masterDataSource") DataSource master,
            @Qualifier("slaveDataSource") DataSource slave,
            @Qualifier("dynamicDataSource") DynamicRoutingDataSource dynamic) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", master);
        targetDataSources.put("slave", slave);

        dynamic.setDefaultTargetDataSource(master);  // 兜底数据源
        dynamic.setTargetDataSources(targetDataSources);
        dynamic.afterPropertiesSet();               // 初始化
        return dynamic;
    }
}

此时整个应用只注入这一个 dynamicDataSource,它表面的行为是一个普通数据源,实际上每次获取连接都会根据当前线程的 key 动态路由。

4. 通过切面在业务方法前设置数据源 key

读写分离的典型做法是:以 getfindquery 开头的方法走从库,其它方法走主库。

@Aspect
@Component
public class DataSourceAspect {

    @Before("execution(* com.example.service..*.*(..))")
    public void before(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        if (methodName.startsWith("get") || methodName.startsWith("find")
                || methodName.startsWith("query")) {
            DataSourceContextHolder.setDataSourceKey("slave");
        } else {
            DataSourceContextHolder.setDataSourceKey("master");
        }
    }

    @After("execution(* com.example.service..*.*(..))")
    public void after() {
        DataSourceContextHolder.clearDataSourceKey(); // 清理,防止线程池复用污染
    }
}

也可以使用自定义注解(如 @ReadOnly)精确标记读操作,然后切面检测该注解来设置数据源,更加可控。

13.6.4 动态数据源的注意事项

1. 事务管理限制

Spring 的事务管理器是在获取连接时绑定数据源的。如果在一个事务中切换了数据源 key,由于事务管理器已经在方法开始时取得了连接,后续切换不会生效。因此动态数据源通常不适合在同一个事务方法内部频繁切换数据源,一般保证一个事务从头到尾使用同一个数据源。读写分离场景下,读操作一般在独立的事务中或使用 @Transactional(readOnly=true) 绑定到从库。

2. ThreadLocal 清理

务必在请求结束后(@After 或拦截器的 afterCompletion)清理 ThreadLocal,否则当 Tomcat 线程池复用线程时,上一次请求残留的 key 会导致数据源错乱。

3. 连接池与健康检查

多数据源场景下,每个数据源应配置合理的连接池大小,并利用 Spring Boot Actuator 或自定义 HealthIndicator 监控各个数据源的可用性,防止某库宕机拖垮整个应用。

4. 分布式事务

跨多个独立数据库的强一致性事务需要借助 JTA 或 Seata 等分布式事务框架。AbstractRoutingDataSource 仅提供切换能力,并不解决分布式事务问题。

13.6.5 实践建议

  • 读写分离 是动态数据源最成熟的应用,多数中间件也提供透明代理,但自建轻量路由仍适合中小规模系统。
  • 多租户 场景下可将租户 ID 作为 key,在拦截器中根据请求参数或 Token 设置数据源,实现数据隔离。
  • 避免过度设计:如果只有两三个固定数据源,静态方式更简单可靠;动态配置适合数据源数量不可预知或需要频繁变动的场合。

通过以上方式,Spring 能优雅地管理多数据源,既保留了面向接口编程的一致性,又满足了数据隔离与性能优化的实际需求。