1. 多数据源连接的核心场景与挑战
在现代企业级应用开发中,一个系统需要同时访问多个数据库的情况越来越普遍。比如你可能需要从MySQL读取用户基础数据,同时又要连接SqlServer获取业务订单信息,还可能涉及Oracle的数据仓库查询。这种多数据源访问的需求主要出现在以下典型场景:
- 遗留系统整合:新旧系统交替阶段,需要同时访问老系统的SqlServer和新系统的MySQL
- 数据隔离需求:核心业务数据与日志数据分别存储在不同数据库实例
- 跨部门协作:不同部门使用不同的数据库技术栈但需要数据互通
- 读写分离场景:写操作使用主库,读操作分散到多个从库
使用SpringBoot实现多数据源连接时,开发者通常会遇到几个关键挑战:
- 数据源配置冲突:多个DataSource实例如何避免Bean名称冲突
- 事务管理难题:跨数据源的事务一致性如何保证
- 会话隔离问题:如何确保不同DAO操作使用正确的数据源
- 性能优化:连接池配置与监控的复杂性成倍增加
2. 基础环境搭建与依赖配置
2.1 项目初始化与必要依赖
创建一个标准的SpringBoot项目(2.7.x版本推荐),在pom.xml中需要包含以下核心依赖:
<dependencies> <!-- SpringBoot基础starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatisPlus starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <scope>runtime</scope> </dependency> <!-- 连接池推荐使用HikariCP --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency> </dependencies>注意:MyBatisPlus版本建议使用3.5.x以上以获得更好的多数据源支持特性
2.2 多数据源配置方案选型
实现多数据源有几种主流方案,各有优缺点:
AbstractRoutingDataSource动态路由方案
- 优点:配置灵活,运行时动态切换
- 缺点:需要自行处理事务管理
独立配置+分包扫描方案
- 优点:结构清晰,各数据源完全隔离
- 缺点:代码冗余,扩展性差
第三方Starter方案(如dynamic-datasource)
- 优点:开箱即用,功能完善
- 缺点:黑盒逻辑,定制困难
本文选择第一种方案,因为它:
- 更适合理解底层原理
- 方便后续自定义扩展
- 不引入额外依赖
3. 核心配置实现详解
3.1 数据源属性配置
在application.yml中配置两个数据源(以MySQL和SqlServer为例):
spring: datasource: master: jdbc-url: jdbc:mysql://localhost:3306/db1?useSSL=false username: root password: mysql123 driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: HikariPool-MySQL maximum-pool-size: 15 slave: jdbc-url: jdbc:sqlserver://localhost:1433;databaseName=db2 username: sa password: sqlserver123 driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver hikari: pool-name: HikariPool-SqlServer maximum-pool-size: 10关键点:使用自定义前缀(master/slave)而非spring.datasource直接配置,避免自动装配冲突
3.2 Java配置类实现
创建多数据源配置类:
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Bean @ConfigurationProperties("spring.datasource.slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Primary @Bean public DataSource dynamicDataSource( @Qualifier("masterDataSource") DataSource master, @Qualifier("slaveDataSource") DataSource slave) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", master); targetDataSources.put("slave", slave); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(master); return dataSource; } }3.3 动态数据源路由实现
创建DynamicDataSource类继承AbstractRoutingDataSource:
public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); public static void setDataSourceKey(String key) { CONTEXT_HOLDER.set(key); } public static void clearDataSourceKey() { CONTEXT_HOLDER.remove(); } @Override protected Object determineCurrentLookupKey() { return CONTEXT_HOLDER.get(); } }4. MyBatisPlus集成与测试
4.1 MyBatisPlus多数据源配置
@Configuration @MapperScan(basePackages = "com.example.mapper") public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setTypeHandlersPackage("com.example.handler"); return sessionFactory.getObject(); } }4.2 数据源切换AOP实现
@Aspect @Component @Order(1) public class DataSourceAspect { @Pointcut("@annotation(com.example.annotation.Master) " + "|| execution(* com.example.service..*.insert*(..)) " + "|| execution(* com.example.service..*.update*(..)) " + "|| execution(* com.example.service..*.delete*(..))") public void masterPointcut() {} @Before("masterPointcut()") public void beforeMaster() { DynamicDataSource.setDataSourceKey("master"); } @Pointcut("@annotation(com.example.annotation.Slave) " + "|| execution(* com.example.service..*.select*(..)) " + "|| execution(* com.example.service..*.get*(..)) " + "|| execution(* com.example.service..*.find*(..))") public void slavePointcut() {} @Before("slavePointcut()") public void beforeSlave() { DynamicDataSource.setDataSourceKey("slave"); } @AfterReturning("masterPointcut() || slavePointcut()") public void afterComplete() { DynamicDataSource.clearDataSourceKey(); } }4.3 测试用例编写
@SpringBootTest class MultiDataSourceTest { @Autowired private UserMapper userMapper; // 使用MySQL数据源 @Autowired private OrderMapper orderMapper; // 使用SqlServer数据源 @Test void testCrossDataSource() { // 默认使用主数据源 User user = userMapper.selectById(1); assertNotNull(user); // 切换数据源查询 DynamicDataSource.setDataSourceKey("slave"); Order order = orderMapper.selectById(1); assertNotNull(order); // 验证事务 try { userMapper.insert(new User(...)); orderMapper.insert(new Order(...)); // 跨数据源操作 fail("Should throw transaction exception"); } catch (Exception e) { assertTrue(e instanceof TransactionException); } } }5. 高级特性与生产级优化
5.1 多数据源事务管理
跨数据源的事务需要引入JTA或Seata等分布式事务解决方案。以Atomikos为例:
@Bean public PlatformTransactionManager transactionManager() throws Exception { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(false); UserTransaction userTransaction = new UserTransactionImp(); userTransaction.setTransactionTimeout(300); return new JtaTransactionManager(userTransaction, userTransactionManager); }5.2 连接池监控配置
@Bean public DataSourcePoolMetadataProvider dataSourcePoolMetadataProvider() { return (dataSource) -> { if (dataSource instanceof HikariDataSource) { return new HikariDataSourcePoolMetadata((HikariDataSource) dataSource); } return null; }; } // 在Actuator中暴露监控端点 management.endpoints.web.exposure.include=health,info,datasource5.3 动态数据源扩展
实现动态添加/移除数据源:
public void addDataSource(String key, DataSource dataSource) { DynamicDataSource dynamicDataSource = (DynamicDataSource) applicationContext.getBean("dynamicDataSource"); Map<Object, Object> targetDataSources = new HashMap<>(dynamicDataSource.getTargetDataSources()); targetDataSources.put(key, dataSource); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.afterPropertiesSet(); }6. 常见问题排查指南
6.1 数据源切换不生效
可能原因:
- AOP顺序问题:确保@Order值小于Spring事务拦截器(通常为Ordered.LOWEST_PRECEDENCE)
- 方法内部调用:自调用不会触发AOP,需要通过代理对象调用
- 注解未正确使用:检查@Master/@Slave注解是否被正确继承
解决方案:
// 显式指定AOP顺序 @EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE) public class AppConfig {}6.2 事务回滚异常
典型错误:
org.springframework.transaction.TransactionSystemException: Could not roll back JDBC transaction; nested exception is java.sql.SQLException: Connection is closed解决方案:
- 配置事务超时时间
- 确保连接池testOnBorrow/testWhileIdle配置正确
- 避免在finally块中手动清理数据源上下文
6.3 MyBatisPlus分页失效
排查步骤:
- 检查拦截器是否正确注入
- 确认不同数据源方言配置正确
- 验证SQL日志是否包含分页语句
// 为不同数据源配置不同方言 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQL_SERVER));7. 性能优化建议
连接池参数调优:
- MySQL推荐配置:maximumPoolSize = (core_count * 2) + effective_spindle_count
- SqlServer推荐配置:maximumPoolSize = 50以内(受限于License)
监控指标收集:
HikariDataSource master = (HikariDataSource)masterDataSource(); log.info("MySQL连接池状态: Active={}, Idle={}, Total={}", master.getHikariPoolMXBean().getActiveConnections(), master.getHikariPoolMXBean().getIdleConnections(), master.getHikariPoolMXBean().getTotalConnections());SQL执行监控:
<settings> <setting name="logImpl" value="SLF4J"/> <setting name="proxyLogLevel" value="debug"/> </settings>多数据源负载均衡:
@Slave @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ReadOnly { String value() default "slave"; boolean loadBalance() default true; }
在实际项目中,我们团队发现当数据源超过3个时,建议引入动态数据源管理中间件。对于读写分离场景,可以结合ShardingSphere实现更复杂的路由策略。