1. 多数据源连接的核心场景与挑战
在企业级应用开发中,多数据源连接是刚需而非炫技。我经历过多个需要同时对接MySQL生产库、SQL Server报表库和Oracle历史数据仓库的项目,这种架构通常出现在以下典型场景:
- 跨系统数据整合(如ERP对接CRM)
- 读写分离与分库分表场景
- 新旧系统并行期数据双写
- 异构数据库同步分析
技术实现上最大的痛点不在于基础配置,而在于如何保证:
- 事务一致性(跨库事务如何协调)
- 连接池管理(避免连接泄漏)
- 动态路由灵活性(根据业务逻辑切换数据源)
踩坑提示:曾有个生产事故是因为没配置连接池最大等待时间,导致高并发时线程堆积。建议必配参数:maxWait、minIdle、validationQuery
2. 技术选型与方案对比
2.1 主流实现方案横评
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| AbstractRoutingDataSource | 轻量级,Spring原生支持 | 需手动处理事务 | 简单多库查询 |
| MyBatis插件 | 细粒度控制 | 侵入性强 | 需要SQL级别路由 |
| ShardingSphere | 功能完备 | 学习成本高 | 分库分表复杂场景 |
经过多次实战验证,对于80%的中等复杂度项目,AbstractRoutingDataSource+MyBatisPlus的组合性价比最高。下面以MySQL 8.0和SQL Server 2019为例演示具体实现。
2.2 依赖版本黄金组合
xml复制<!-- 关键依赖版本锁死,避免兼容性问题 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>11.2.1.jre8</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
血泪教训:SQL Server驱动必须用jre8版本,否则会出现TDS协议不兼容问题
3. 核心实现三部曲
3.1 数据源配置的军规写法
yaml复制# application-multi-db.yaml
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/core_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 加密密码建议用Jasypt
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
connection-timeout: 30000
maximum-pool-size: 20
slave:
url: jdbc:sqlserver://192.168.1.100:1433;databaseName=report_db
username: sa
password: 复杂密码
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
hikari:
max-lifetime: 1800000
idle-timeout: 600000
配置要点:
- MySQL必须带时区参数
- SQL Server的URL格式特殊(分号分隔)
- 连接池参数必须按业务负载调整
3.2 动态数据源路由引擎
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
// 优雅清除ThreadLocal的模板方法
public static void clear() {
CONTEXT_HOLDER.remove();
}
}
配套的配置类需要:
- 初始化所有数据源
- 设置默认数据源
- 配置事务管理器
java复制@Configuration
@MapperScan(basePackages = "com.**.mapper")
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
public DataSource dynamicDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
DynamicDataSource ds = new DynamicDataSource();
ds.setTargetDataSources(targetDataSources);
ds.setDefaultTargetDataSource(masterDataSource());
return ds;
}
// 需要为每个数据源配置独立的事务管理器
@Bean
public PlatformTransactionManager txManager(DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
3.3 MyBatisPlus的多源适配技巧
Mapper接口分包管理是规范化的关键:
code复制src/main/java
└── com
└── example
├── master
│ └── mapper
│ └── UserMapper.java
└── slave
└── mapper
└── ReportMapper.java
对应的扫描配置:
java复制@Configuration
public class MybatisPlusConfig {
@Bean("masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource)
throws Exception {
return createSqlSessionFactory(dataSource, "classpath:mapper/master/**/*.xml");
}
@Bean("slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource)
throws Exception {
return createSqlSessionFactory(dataSource, "classpath:mapper/slave/**/*.xml");
}
private SqlSessionFactory createSqlSessionFactory(DataSource dataSource, String mapperLocations)
throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(mapperLocations));
// 其他自定义配置...
return sessionFactory.getObject();
}
}
4. 实战中的高阶技巧
4.1 事务处理的正确姿势
跨库事务必须使用分布式事务方案,但在单服务多数据源场景下,可以用以下模式缓解:
java复制@Service
public class OrderService {
@Transactional(transactionManager = "txManager")
public void createOrder(Order order) {
// 默认使用主库
orderMapper.insert(order);
try {
DynamicDataSource.setDataSourceKey("slave");
// 从库操作
reportMapper.updateSales(order.getAmount());
} finally {
DynamicDataSource.clear();
}
}
}
重要:必须用try-finally确保线程变量清除,否则会导致后续请求错用数据源
4.2 动态切换的AOP优化
通过注解实现优雅切换:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceSelector {
String value() default "master";
}
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(selector)")
public void beforeSwitch(JoinPoint point, DataSourceSelector selector) {
DynamicDataSource.setDataSourceKey(selector.value());
}
@After("@annotation(selector)")
public void afterSwitch(DataSourceSelector selector) {
DynamicDataSource.clear();
}
}
使用示例:
java复制@DataSourceSelector("slave")
public List<Report> getDailyReport(Date date) {
return reportMapper.selectByDate(date);
}
4.3 性能监控与调优
建议增加以下监控指标:
- 连接池活跃连接数
- SQL执行时间分布
- 事务提交/回滚统计
Spring Boot Actuator配置示例:
yaml复制management:
endpoints:
web:
exposure:
include: health,metrics,datasource
metrics:
distribution:
percentiles:
hikari:
connections:
acquire: 0.5,0.9,0.99
5. 生产环境避坑指南
5.1 常见故障排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 连接泄漏 | 未清除ThreadLocal | 添加AOP清理逻辑 |
| SQL Server连接超时 | 驱动版本不匹配 | 使用jre8兼容驱动 |
| 事务不生效 | 错误的事务管理器 | 配置多事务管理器 |
| MyBatis缓存污染 | 未隔离的二级缓存 | 为每个数据源配置独立缓存 |
5.2 连接池参数黄金法则
java复制HikariConfig config = new HikariConfig();
// 关键参数设置公式
config.setMaximumPoolSize(CPU核心数 * 2 + 有效磁盘数);
config.setConnectionTimeout(平均查询时间 * 3);
config.setIdleTimeout(业务低谷间隔时间 / 2);
5.3 异构数据库兼容处理
不同数据库的SQL差异处理方案:
- 使用MyBatis的databaseIdProvider
xml复制<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
</databaseIdProvider>
- 在Mapper XML中区分:
xml复制<select id="findUsers" databaseId="mysql">
SELECT * FROM users WHERE create_time > #{date}
</select>
<select id="findUsers" databaseId="sqlserver">
SELECT * FROM users WHERE create_time > CONVERT(DATETIME, #{date})
</select>
6. 扩展思考:云原生下的多数据源
在Kubernetes环境中,建议:
- 通过ConfigMap管理不同环境的连接配置
- 使用Sidecar模式处理数据库协议转换
- 通过Service Mesh实现数据库访问的熔断
本地开发时可以用Testcontainers实现多数据库集成测试:
java复制@Testcontainers
class MultiDataSourceTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@Container
static MSSQLServerContainer<?> sqlserver = new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2019-latest");
@Test
void testCrossDatabaseQuery() {
// 测试逻辑
}
}
多数据源配置看似简单,但在高并发分布式环境下,每个选择都关系到系统稳定性。经过多个项目的锤炼,我的经验法则是:简单查询用动态路由,复杂业务用领域拆分,核心交易用分布式事务中间件。