1. 为什么需要多数据源支持?
在企业级应用开发中,单一数据库往往无法满足所有业务需求。我最近接手的一个电商后台系统就面临这样的场景:订单数据存储在SQL Server中(历史原因),而用户行为日志则使用PostgreSQL(更适合JSON数据处理)。这种异构数据库环境在金融、医疗等行业尤为常见。
多数据源配置的核心挑战在于:
- 事务一致性管理(跨库事务)
- 连接池资源分配
- 动态路由策略
- ORM框架适配
Spring Boot通过抽象化的DataSource配置,配合MyBatis/Hibernate等ORM框架,为这些挑战提供了优雅的解决方案。下面这个对比表展示了常见场景下的数据源选择策略:
| 业务场景 | 推荐数据源 | 理由 |
|---|---|---|
| 高频交易 | SQL Server | 商业数据库的事务保障 |
| 日志分析 | PostgreSQL | JSONB类型和地理空间支持 |
| 报表查询 | 读写分离从库 | 避免影响主库性能 |
| 缓存数据 | Redis | 高速读写 |
提示:在多数据源配置前,务必评估业务真正的隔离需求。过度设计会导致维护成本指数级上升。
2. 基础环境搭建
2.1 依赖配置
在pom.xml中需要同时引入两种数据库驱动(以Spring Boot 2.7.x为例):
xml复制<!-- PostgreSQL -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.4</version>
</dependency>
<!-- SQL Server -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>11.2.0.jre8</version>
</dependency>
<!-- 数据源核心依赖 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
2.2 配置文件详解
application.yml需要定义两个独立的数据源配置。这里有个关键细节:必须禁用Spring Boot的自动数据源初始化:
yaml复制spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
datasource:
postgres:
jdbc-url: jdbc:postgresql://localhost:5432/log_db
username: pgadmin
password: securepass
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
connection-timeout: 30000
sqlserver:
jdbc-url: jdbc:sqlserver://localhost:1433;databaseName=order_db
username: sa
password: SqlServer2022!
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
hikari:
maximum-pool-size: 15
connection-timeout: 20000
注意:SQL Server的jdbc-url格式特殊,分号作为参数分隔符必须保留。我在实际项目中曾因漏写分号导致8小时的排查耗时。
3. 数据源配置类实现
3.1 主数据源定义
创建PostgreSQL作为主数据源(默认数据源):
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper.pg",
sqlSessionFactoryRef = "postgresSessionFactory")
public class PostgresDataSourceConfig {
@Bean(name = "postgresDataSource")
@ConfigurationProperties(prefix = "spring.datasource.postgres")
public DataSource postgresDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean(name = "postgresSessionFactory")
public SqlSessionFactory postgresSessionFactory(
@Qualifier("postgresDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/pg/*.xml"));
return bean.getObject();
}
}
3.2 次数据源配置
SQL Server作为次要数据源的配置需要额外注意事务管理器隔离:
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper.mssql",
sqlSessionFactoryRef = "sqlServerSessionFactory")
public class SqlServerDataSourceConfig {
@Bean(name = "sqlServerDataSource")
@ConfigurationProperties(prefix = "spring.datasource.sqlserver")
public DataSource sqlServerDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean(name = "sqlServerTransactionManager")
public PlatformTransactionManager sqlServerTransactionManager(
@Qualifier("sqlServerDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "sqlServerSessionFactory")
public SqlSessionFactory sqlServerSessionFactory(
@Qualifier("sqlServerDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setTransactionFactory(new SpringManagedTransactionFactory());
bean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/mssql/*.xml"));
return bean.getObject();
}
}
3.3 动态数据源路由
对于需要运行时切换数据源的场景,可以配置AbstractRoutingDataSource:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
@Component
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
使用时通过注解切换:
java复制@Service
public class OrderService {
@Transactional(transactionManager = "sqlServerTransactionManager")
public void createOrder(Order order) {
// 使用SQL Server数据源
}
}
4. 实战中的疑难问题
4.1 连接泄漏排查
在多数据源环境下,连接泄漏问题会被放大。我的排查方案:
- 启用HikariCP的监控端点:
yaml复制management:
endpoints:
web:
exposure:
include: hikaricp
- 检查活跃连接数:
bash复制curl http://localhost:8080/actuator/hikaricp | jq '.datasources[].pool'
- 常见泄漏场景:
- 未关闭的ResultSet/Statement
- 事务未正确回滚或提交
- 线程池任务未释放连接
4.2 跨库事务处理
对于需要跨库事务的业务,有几种折中方案:
- 最终一致性模式:
java复制public void placeOrder(Order order, LogEntry log) {
// 先操作主库
orderMapper.insert(order);
// 异步记录日志
logQueue.add(log);
}
- JTA分布式事务(性能损耗大):
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
- Saga模式实现:
java复制@SagaStart
public void distributedOperation() {
try {
step1();
step2();
} catch(Exception e) {
compensate();
}
}
4.3 性能优化要点
根据压测经验,给出以下参数建议:
| 参数 | PostgreSQL建议值 | SQL Server建议值 |
|---|---|---|
| maximumPoolSize | CPU核心数*2 | CPU核心数*3 |
| minimumIdle | 0 | 5 |
| idleTimeout | 600000ms | 300000ms |
| maxLifetime | 1800000ms | 1200000ms |
| connectionTimeout | 30000ms | 20000ms |
关键发现:SQL Server的连接建立成本更高,需要适当增加minimumIdle
5. 高级应用场景
5.1 读写分离扩展
基于AbstractRoutingDataSource可以实现更复杂的路由策略:
java复制public class ReadWriteDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
String methodName = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest().getMethod();
if ("GET".equalsIgnoreCase(methodName) || isReadOnly) {
return "read";
}
return "write";
}
}
5.2 多租户支持
结合数据源路由实现SaaS多租户:
java复制public class TenantDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String tenantId = TenantContext.getCurrentTenant();
if (!dataSourceMap.containsKey(tenantId)) {
synchronized(this) {
if (!dataSourceMap.containsKey(tenantId)) {
createNewDataSource(tenantId);
}
}
}
return tenantId;
}
}
5.3 数据源健康检查
自定义健康检查端点:
java复制@Component
public class DataSourceHealthIndicator implements HealthIndicator {
@Autowired
@Qualifier("postgresDataSource")
private DataSource pgDataSource;
@Autowired
@Qualifier("sqlServerDataSource")
private DataSource msDataSource;
@Override
public Health health() {
Map<String, Health> details = new LinkedHashMap<>();
details.put("postgres", checkDataSource(pgDataSource));
details.put("sqlserver", checkDataSource(msDataSource));
return Health.status(
details.values().stream().allMatch(h -> h.getStatus().equals(Status.UP))
? Status.UP : Status.DOWN)
.withDetails(details)
.build();
}
private Health checkDataSource(DataSource dataSource) {
try (Connection conn = dataSource.getConnection()) {
return Health.up()
.withDetail("version", conn.getMetaData().getDatabaseProductVersion())
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
6. 监控与维护
6.1 Prometheus监控配置
application.yml添加:
yaml复制management:
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
Grafana监控面板建议指标:
hikaricp_connections_activehikaricp_connections_idlehikaricp_connections_pendingjdbc_connections_created
6.2 连接池调优
通过JMX实时调整参数:
java复制@Bean
public HikariPoolMXBean postgresPoolMXBean(
@Qualifier("postgresDataSource") DataSource dataSource) {
return ((HikariDataSource) dataSource).getHikariPoolMXBean();
}
常用调优命令:
java复制// 动态修改连接池大小
poolMXBean.setMaximumPoolSize(20);
// 软重置连接池
poolMXBean.softEvictConnections();
6.3 故障转移方案
配置备用数据源:
java复制@Bean(name = "postgresDataSource")
public DataSource postgresDataSource() {
HikariDataSource primary = buildDataSource("primary");
HikariDataSource secondary = buildDataSource("secondary");
return new FailoverDataSource(primary, secondary);
}
private HikariDataSource buildDataSource(String type) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(env.getProperty("spring.datasource."+type+".jdbc-url"));
// 其他配置...
return new HikariDataSource(config);
}
7. 项目结构建议
合理的多数据源项目应该遵循以下结构:
code复制src/main/java
├── config
│ ├── datasource
│ │ ├── PostgresDataSourceConfig.java
│ │ └── SqlServerDataSourceConfig.java
│ └── DynamicDataSourceConfig.java
├── mapper
│ ├── pg
│ │ └── LogMapper.java
│ └── mssql
│ └── OrderMapper.java
└── service
├── impl
│ ├── LogServiceImpl.java
│ └── OrderServiceImpl.java
└── routing
└── DataSourceContextHolder.java
这种结构的好处是:
- 配置类集中管理
- Mapper按数据源物理隔离
- 服务层通过包名显式区分职责
8. 版本兼容性备忘
不同Spring Boot版本的关键差异:
| Spring Boot版本 | 注意事项 |
|---|---|
| 2.4.x及之前 | 需要手动排除DataSourceAutoConfiguration |
| 2.5.x | 引入spring.sql.init.mode=never替代旧配置 |
| 2.7.x | HikariCP默认连接超时改为30秒 |
| 3.0.x | 需要JDK17+,SQL Server驱动包名变更 |
特别提醒:SQL Server 2016以下版本需要使用jtds驱动而非mssql-jdbc:
xml复制<dependency>
<groupId>net.sourceforge.jtds</groupId>
<artifactId>jtds</artifactId>
<version>1.3.1</version>
</dependency>
9. 测试策略
9.1 单元测试配置
测试类需要显式指定数据源:
java复制@SpringBootTest
@TestPropertySource(locations = "classpath:test-application.yml")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Transactional(transactionManager = "sqlServerTransactionManager")
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
public void testCreateOrder() {
// 测试代码
}
}
9.2 集成测试方案
使用Testcontainers实现真实环境测试:
java复制@Testcontainers
public class MultiDataSourceIntegrationTest {
@Container
static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:13");
@Container
static MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2019-latest");
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.postgres.jdbc-url", pg::getJdbcUrl);
registry.add("spring.datasource.sqlserver.jdbc-url", mssql::getJdbcUrl);
}
@Test
void testCrossDatabaseOperation() {
// 测试代码
}
}
10. 生产环境检查清单
部署前必须验证的项目:
- [ ] 连接池监控接口已保护
- [ ] 数据源密码已加密
- [ ] 防火墙开放了对应端口
- [ ] 连接泄漏检测机制已启用
- [ ] 故障转移方案经过验证
- [ ] 慢查询日志阈值设置合理
- [ ] 连接池参数经过压力测试
我在实际部署中总结的最佳实践:
- 为每个数据源配置独立的HikariCP连接池名称,便于监控区分
- 生产环境禁用JMX远程访问,改用Prometheus拉取
- SQL Server建议启用prepareThreshold=2优化性能
- PostgreSQL配置中设置preparedStatementCacheQueries=1000
