当单一数据库不再能满足业务需求时,拆分数据到不同库或连接不同业务系统就成了常态。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
读写分离的典型做法是:以 get、find、query 开头的方法走从库,其它方法走主库。
@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 能优雅地管理多数据源,既保留了面向接口编程的一致性,又满足了数据隔离与性能优化的实际需求。