1. 多数据源场景的现实需求
在企业级应用开发中,多数据源的需求远比我们想象的更加普遍。去年我接手的一个电商平台改造项目,就遇到了必须同时连接商品库、订单库和用户库的情况。这种架构在以下场景中尤为常见:
- 业务模块垂直拆分:不同业务线使用独立数据库实例
- 读写分离部署:主库负责写操作,从库承担读请求
- 跨系统数据集成:需要同时访问多个外部系统的数据
- 多租户SaaS应用:每个租户拥有独立的数据存储空间
Spring Boot的自动配置机制默认只支持单个数据源,这显然无法满足上述复杂场景。手动管理多个DataSource虽然可行,但会陷入大量模板代码的泥潭。我们需要一套既保持Spring Boot简洁特性,又能灵活切换数据源的解决方案。
2. 基础环境搭建与依赖配置
2.1 必要依赖引入
在pom.xml中,除了基础的spring-boot-starter-jdbc,我们还需要引入:
xml复制<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
HikariCP作为目前性能最优的连接池实现,是生产环境的首选。而AOP模块则是实现动态切换的关键技术基础。
2.2 多数据源配置策略
在application.yml中采用以下配置结构:
yaml复制spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/main_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
jdbc-url: jdbc:mysql://192.168.1.100:3306/log_db
username: log_user
password: log@123
driver-class-name: com.mysql.cj.jdbc.Driver
这种命名空间隔离的配置方式,比传统的spring.datasource.url写法更利于多数据源管理。注意这里使用了jdbc-url而非url,这是为了避免HikariCP的配置冲突。
3. 数据源初始化与Bean管理
3.1 配置类设计模式
创建DataSourceConfig配置类,采用显式Bean声明方式:
java复制@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
这里有几个关键点需要注意:
- 明确指定HikariDataSource类型以获得最佳性能
- @ConfigurationProperties将自动绑定对应前缀的配置项
- 方法名将作为Bean的名称标识
3.2 事务管理器特殊处理
默认的PlatformTransactionManager只能处理单个数据源,我们需要自定义:
java复制@Bean
public PlatformTransactionManager transactionManager(
@Qualifier("primaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
这里选择主数据源作为默认事务管理器,其他数据源的事务需要通过编程式事务管理。在需要严格事务控制的场景,可以考虑引入JTA分布式事务解决方案。
4. 动态数据源路由实现
4.1 抽象路由数据源
核心是继承AbstractRoutingDataSource:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER =
new NamedThreadLocal<>("RoutingDataSourceContext");
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
}
ThreadLocal保证了线程隔离性,每个请求线程都能独立维护自己的数据源选择。NamedThreadLocal提供了更好的调试信息。
4.2 数据源切换AOP切面
通过注解实现优雅切换:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "primary";
}
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void beforeSwitch(DataSource dataSource) {
DynamicDataSource.setDataSourceKey(dataSource.value());
}
@After("@annotation(dataSource)")
public void afterSwitch(DataSource dataSource) {
DynamicDataSource.setDataSourceKey("primary");
}
}
这种实现方式相比基于方法名的规则匹配更加直观可控。注解可以精确到方法级别,且支持类级别的默认设置。
5. 实战应用与性能优化
5.1 服务层使用示例
在Service方法上简单标注即可:
java复制@Service
public class OrderService {
@DataSource("primary")
public void createOrder(Order order) {
// 使用主库执行写操作
}
@DataSource("secondary")
public List<Order> queryOrders(Long userId) {
// 使用从库执行查询
}
}
5.2 连接池参数调优
不同业务场景可能需要不同的连接池配置:
yaml复制spring:
datasource:
primary:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
secondary:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 10000
写操作为主的primary池可以设置较小的连接数,而读密集的secondary池则需要更大的连接规模。超时时间也应根据业务容忍度调整。
6. 生产环境注意事项
6.1 多数据源事务陷阱
动态数据源切换与Spring声明式事务存在执行顺序问题。解决方案包括:
- 将@Transactional注解放在@DataSource之前
- 使用Ordered接口调整切面执行顺序
- 在事务方法内部手动切换数据源
6.2 监控与健康检查
需要为每个数据源单独配置健康指示器:
java复制@Bean
public HealthIndicator primaryHealth() {
return new DataSourceHealthIndicator(primaryDataSource());
}
@Bean
public HealthIndicator secondaryHealth() {
return new DataSourceHealthIndicator(secondaryDataSource());
}
这样在/actuator/health端点可以分别查看各数据源状态。建议结合Prometheus配置连接池使用率告警。
7. 高级扩展方案
7.1 基于分库分表的动态路由
可以扩展determineCurrentLookupKey逻辑,实现更复杂的路由策略:
java复制protected Object determineCurrentLookupKey() {
String userId = UserContext.getCurrentUserId();
int hash = Math.abs(userId.hashCode()) % 3;
return "shard_" + hash;
}
这种模式常用于水平分片场景,根据业务ID自动路由到对应的分片数据库。
7.2 多租户数据源管理
结合租户上下文实现动态数据源加载:
java复制public void addTenantDataSource(String tenantId) {
DataSource dataSource = buildDataSourceForTenant(tenantId);
targetDataSources.put(tenantId, dataSource);
afterPropertiesSet(); // 刷新路由数据源
}
这种方案可以在运行时动态添加新的租户数据源,适合SaaS应用场景。需要特别注意连接泄露问题。