Spring Boot多数据源配置实战:手动装配与MyBatis集成详解
1. 项目概述为什么需要配置多个数据源在真实的业务开发中尤其是面对中大型项目时单一数据库往往无法满足所有需求。你可能遇到过这样的场景核心交易数据需要存储在性能强劲的MySQL主库中而用于报表分析的大量历史数据则存放在另一个独立的MySQL实例甚至是一个专门的ClickHouse或PostgreSQL数据库中。又或者你的应用需要同时连接一个用于业务操作的事务型数据库和一个仅用于查询的只读副本以实现读写分离。这些场景都指向一个共同的技术需求在一个Spring Boot应用中如何同时管理和使用多个数据源。Spring Boot以其“约定大于配置”的理念极大地简化了开发其spring-boot-starter-data-jdbc或spring-boot-starter-data-jpa等启动器为我们自动配置了一个默认数据源。但当我们需要第二个、第三个数据源时自动配置就“失灵”了。这时我们就需要手动介入清晰地告诉Spring“这里有两个或多个不同的数据库连接请分别管理它们。”这个过程就是多数据源配置的核心。它不仅仅是创建几个DataSourceBean那么简单更深层次的是要解决事务管理、MyBatis Mapper或JPA Repository的绑定、连接池隔离等一系列问题。理解并掌握它是从“会用Spring Boot”到“能驾驭复杂企业应用”的关键一步。2. 核心设计思路与方案选型配置多数据源本质上是在Spring的IoC容器中注册多个互不干扰的、完整的数据访问组件集。一个完整的数据访问组件集通常包括DataSource数据源、PlatformTransactionManager事务管理器、以及数据访问模板如JdbcTemplate或ORM框架的会话工厂如SqlSessionFactory。2.1 主流方案对比在实际项目中主要有两种实现思路方案一基于配置类的手动装配这是最经典、最灵活也是理解原理最透彻的方式。你需要为每一个数据源编写独立的配置类或在一个配置类中使用Bean方法显式地定义该数据源所需的全部Bean并通过Primary注解指定默认数据源通过Qualifier或自定义注解进行区分。这种方案的优势在于控制力极强你可以为每个数据源精细地配置不同的连接池参数、事务传播行为等。缺点是配置量稍大每个数据源都需要一套模板化的代码。方案二借助第三方Starter如dynamic-datasource-spring-boot-starter这是一个非常流行的开源项目它通过一个注解如DS(“slave”)来动态切换数据源极大地简化了配置。你只需要在配置文件中定义多个数据源然后在Service或Mapper层的方法上添加注解即可。这种方案对于读写分离、多租户等场景特别友好使用起来非常便捷。但它的封装也带来了一定的“黑盒”性遇到复杂事务或特定整合需求时可能需要深入理解其内部机制。对于学习和深入理解Spring Boot数据访问底层原理而言手动装配方案是必经之路。它能让你清晰地看到每一个组件是如何被创建和关联的为后续解决复杂问题打下坚实基础。因此本文将重点详解基于配置类的手动装配方案。2.2 项目结构与依赖规划在开始编码前一个清晰的项目结构能避免后续的混乱。我们假设一个典型场景应用需要连接两个MySQL数据库一个主库primary_db用于核心业务CUD操作一个从库secondary_db用于查询和报表。首先确保你的pom.xml包含了必要的依赖。除了基础的Web启动器我们还需要JDBC、MySQL驱动以及常用的连接池如HikariCPSpring Boot默认已集成。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jdbc/artifactId /dependency !-- 使用MyBatis则替换为 mybatis-spring-boot-starter -- !-- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies注意spring-boot-starter-data-jdbc已经自带了HikariCP连接池无需额外引入。如果你倾向于使用其他连接池如Druid则需要排除HikariCP并引入对应的依赖。在application.yml中我们先规划好两个数据源的连接信息。这里的关键是不能使用Spring Boot自动配置的spring.datasource.url等前缀因为那只会自动配置一个主数据源。我们需要使用自定义的前缀。# 应用基础配置 server: port: 8080 # 主数据源配置 (primary_db) custom: datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/primary_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # HikariCP连接池配置可选覆盖默认值 hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 # 从数据源配置 (secondary_db) secondary: jdbc-url: jdbc:mysql://localhost:3307/secondary_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 5 # 从库连接池可以小一些这里我使用了custom.datasource.primary和custom.datasource.secondary作为自定义前缀。你完全可以使用spring.datasource.primary但为了避免与自动配置的潜在冲突或混淆使用一个完全独立的顶级前缀是更清晰的做法。3. 核心细节解析与配置要点手动配置多数据源的核心在于创建多个互不干扰的“数据访问上下文”。每一个上下文都需要独立配置并且要妥善处理它们之间的“竞争”关系尤其是当Spring的自动配置试图帮忙却帮了倒忙的时候。3.1 数据源配置类设计我们将创建两个配置类分别对应主数据源和从数据源。但更优雅的做法是创建一个主配置类然后用内部类或独立的Configuration类来隔离不同数据源的配置。这里采用内部静态类的形式结构更紧凑。Configuration public class DataSourceConfiguration { // 主数据源配置 Configuration ConfigurationProperties(prefix custom.datasource.primary) // 绑定配置属性 public class PrimaryDataSourceConfig { // 声明一个Bean方法名即Bean的名称这里命名为primaryDataSource Bean(name primaryDataSource) Primary // 关键指定该数据源为默认数据源 public DataSource primaryDataSource() { // 使用HikariDataSourceSpring Boot会自动根据ConfigurationProperties绑定的属性创建 return DataSourceBuilder.create().type(HikariDataSource.class).build(); } // 为主数据源配置事务管理器Bean名称设为primaryTransactionManager Bean(name primaryTransactionManager) Primary // 同样默认事务管理器也指定为主库的 public PlatformTransactionManager primaryTransactionManager(Qualifier(primaryDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } // 如果需要使用JdbcTemplate也可以在这里配置一个 Bean(name primaryJdbcTemplate) Primary public JdbcTemplate primaryJdbcTemplate(Qualifier(primaryDataSource) DataSource dataSource) { return new JdbcTemplate(dataSource); } } // 从数据源配置 Configuration ConfigurationProperties(prefix custom.datasource.secondary) public class SecondaryDataSourceConfig { Bean(name secondaryDataSource) public DataSource secondaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } Bean(name secondaryTransactionManager) public PlatformTransactionManager secondaryTransactionManager(Qualifier(secondaryDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } Bean(name secondaryJdbcTemplate) public JdbcTemplate secondaryJdbcTemplate(Qualifier(secondaryDataSource) DataSource dataSource) { return new JdbcTemplate(dataSource); } } }关键点解析ConfigurationProperties这个注解是Spring Boot的“魔法”之一。它将application.yml中custom.datasource.primary前缀下的所有属性jdbc-url,username,password,hikari.*等自动绑定到DataSourceBuilder创建的过程中无需我们手动调用setUrl()、setUsername()等方法。Primary注解这是多数据源配置中的灵魂。Spring在注入DataSource或PlatformTransactionManager时如果发现有多个同类型的Bean且没有通过Qualifier指定就会抛出NoUniqueBeanDefinitionException异常。Primary告诉Spring“当有多个候选Bean时优先选择我。”通常我们会将业务上最主要、最常用的那个数据源标记为Primary。Qualifier注解当需要注入非默认非Primary的Bean时就必须使用Qualifier通过Bean的名称来明确指定。例如在secondaryTransactionManager方法中参数DataSource dataSource前加了Qualifier(“secondaryDataSource”)这确保了它注入的是我们刚刚定义的从数据源而不是被Primary标记的主数据源。Bean的命名显式地使用Bean(name “…”为每个Bean指定一个清晰的名称是一个好习惯。这使代码在引用时更清晰也便于在Qualifier中使用。3.2 与MyBatis集成如果你的项目使用MyBatis作为ORM框架配置会稍微复杂一点因为你需要为每个数据源配置独立的SqlSessionFactory和MapperScannerConfigurer。首先在对应的配置类中例如PrimaryDataSourceConfig增加MyBatis相关的Bean定义Configuration ConfigurationProperties(prefix custom.datasource.primary) MapperScan(basePackages com.yourproject.mapper.primary, // 指定Mapper接口所在的包 sqlSessionFactoryRef primarySqlSessionFactory) // 指定使用的SqlSessionFactory public class PrimaryDataSourceConfig { Bean(name primaryDataSource) Primary public DataSource primaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } Bean(name primarySqlSessionFactory) Primary public SqlSessionFactory primarySqlSessionFactory(Qualifier(primaryDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactory new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); // 设置MyBatis配置文件路径可选 // sessionFactory.setConfigLocation(new ClassPathResource(mybatis-config.xml)); // 设置实体类别名包推荐 sessionFactory.setTypeAliasesPackage(com.yourproject.entity.primary); // 设置Mapper XML文件位置 sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(classpath:mapper/primary/*.xml)); return sessionFactory.getObject(); } Bean(name primaryTransactionManager) Primary public PlatformTransactionManager primaryTransactionManager(Qualifier(primaryDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } // 如果需要也可以配置SqlSessionTemplate Bean(name primarySqlSessionTemplate) Primary public SqlSessionTemplate primarySqlSessionTemplate(Qualifier(primarySqlSessionFactory) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }关键点解析MapperScan这个注解用于自动扫描指定包下的Mapper接口并将其注册为Spring Bean。sqlSessionFactoryRef属性至关重要它告诉Spring这个包下的Mapper应该使用哪个SqlSessionFactory即绑定到哪个数据源。包路径隔离这是实现MyBatis多数据源最清晰的方式。将不同数据源对应的Mapper接口、实体类、XML映射文件分别放在不同的包下如mapper.primary,entity.primary,mapper.secondary,entity.secondary。这样在配置MapperScan和setMapperLocations时可以做到物理隔离一目了然。SqlSessionFactoryBean这是创建MyBatis核心对象SqlSessionFactory的工厂Bean。我们必须手动创建它并将对应的数据源设置进去。实操心得在与MyBatis集成时最容易出错的地方就是Mapper接口和XML文件与SqlSessionFactory的绑定错乱。务必确保MapperScan的basePackages、sqlSessionFactoryRef以及SqlSessionFactoryBean的setMapperLocations路径三者指向的是同一组资源。采用分包的策略能极大降低出错概率。4. 实操过程与核心环节实现配置写好了接下来就是在业务代码中如何使用它们。这里的关键在于如何准确注入你想要的那个数据源或相关组件。4.1 在Service层中使用指定数据源假设我们有一个UserService需要从主库写入用户信息并从从库查询用户列表。方案A使用Qualifier注入特定的JdbcTemplateService public class UserService { private final JdbcTemplate primaryJdbcTemplate; private final JdbcTemplate secondaryJdbcTemplate; // 通过构造器注入并使用Qualifier指定Bean名称 public UserService(Qualifier(primaryJdbcTemplate) JdbcTemplate primaryJdbcTemplate, Qualifier(secondaryJdbcTemplate) JdbcTemplate secondaryJdbcTemplate) { this.primaryJdbcTemplate primaryJdbcTemplate; this.secondaryJdbcTemplate secondaryJdbcTemplate; } public void createUser(User user) { // 使用主库的JdbcTemplate执行插入 String sql INSERT INTO user(name, email) VALUES (?, ?); primaryJdbcTemplate.update(sql, user.getName(), user.getEmail()); } public ListUser getAllUsers() { // 使用从库的JdbcTemplate执行查询 String sql SELECT * FROM user; return secondaryJdbcTemplate.query(sql, new BeanPropertyRowMapper(User.class)); } }方案B使用Qualifier注入特定的DataSource并动态创建JdbcTemplate不推荐Service public class ReportService { private final DataSource secondaryDataSource; public ReportService(Qualifier(secondaryDataSource) DataSource secondaryDataSource) { this.secondaryDataSource secondaryDataSource; } public void generateComplexReport() { // 在方法内部临时创建JdbcTemplate适用于极少数特殊场景 JdbcTemplate jdbcTemplate new JdbcTemplate(secondaryDataSource); // ... 执行复杂报表查询 } }注意方案B通常不推荐因为每次调用都创建新的JdbcTemplate实例会带来不必要的开销。最佳实践是在配置类中一次性创建好这些模板Bean然后在Service中注入使用。4.2 事务管理在多数据源下的应用事务管理是另一个需要特别注意的点。我们为每个数据源都配置了独立的事务管理器primaryTransactionManager,secondaryTransactionManager。在声明事务时必须指定使用哪个事务管理器。Service public class OrderService { Transactional(transactionManager primaryTransactionManager) // 指定使用主库事务管理器 public void createOrder(Order order) { // 业务逻辑所有数据库操作默认都在主数据源上并受此事务管理 // ... } } Service public class LogQueryService { Transactional(transactionManager secondaryTransactionManager, readOnly true) // 指定使用从库事务管理器并设为只读 public ListAccessLog queryLogs(Date date) { // 查询操作使用从库只读事务有助于数据库优化 // ... } }如果你在一个Service方法中需要跨多个数据源操作并保持原子性分布式事务那么单纯的Transactional就无能为力了。这涉及到更复杂的分布式事务解决方案如Seata、基于消息的最终一致性等这超出了本文的范围。在多数据源架构下通常需要从业务设计上避免跨库事务。4.3 使用MyBatis Mapper当按照前述方式配置好MyBatis后使用Mapper就非常简单了。Spring会根据MapperScan的配置自动将对应包下的接口与正确的SqlSessionFactory关联。// 文件位置com.yourproject.mapper.primary.UserMapper // 这个接口被PrimaryDataSourceConfig中的MapperScan扫描到并绑定到primarySqlSessionFactory Mapper // 或由MapperScan自动注册此注解可省略 public interface PrimaryUserMapper { Insert(INSERT INTO user(name, email) VALUES (#{name}, #{email})) Options(useGeneratedKeys true, keyProperty id) int insert(User user); Select(SELECT * FROM user WHERE id #{id}) User selectById(Long id); } // 文件位置com.yourproject.mapper.secondary.LogMapper // 这个接口需要另一个配置类SecondaryDataSourceConfig中的MapperScan来扫描 public interface SecondaryLogMapper { Select(SELECT * FROM access_log WHERE create_time #{startTime}) ListAccessLog selectAfterTime(Date startTime); }然后在Service中直接注入对应的Mapper即可Service public class BusinessService { private final PrimaryUserMapper primaryUserMapper; private final SecondaryLogMapper secondaryLogMapper; public BusinessService(PrimaryUserMapper primaryUserMapper, SecondaryLogMapper secondaryLogMapper) { this.primaryUserMapper primaryUserMapper; this.secondaryLogMapper secondaryLogMapper; } // ... 使用mapper进行数据库操作 }由于Mapper接口位于不同的包下并且被不同的MapperScan配置指向不同的SqlSessionFactorySpring能够正确地将它们注入并连接到各自的数据源。5. 常见问题与排查技巧实录即使按照步骤配置在实际开发中还是会遇到一些“坑”。下面记录了几个最常见的问题及其解决方法。5.1 启动时报错Consider marking one of the beans as Primary错误信息*************************** APPLICATION FAILED TO START *************************** Description: Field dataSource in com.zaxxer.hikari.HikariDataSource required a single bean, but 2 were found: - primaryDataSource: defined by method primaryDataSource in class path resource [...] - secondaryDataSource: defined by method secondaryDataSource in class path resource [...] Action: Consider marking one of the beans as Primary, updating the consumer to accept multiple beans, or using Qualifier to identify the bean that should be consumed原因与解决 这个错误非常明确Spring发现了多个DataSource类型的Bean但有些地方可能是HikariCP内部也可能是其他自动配置的组件需要注入一个DataSource时它不知道选哪个。解决方法就是按照错误提示在你希望作为默认数据源的Bean上添加Primary注解。通常你的主要业务数据库应该被标记为Primary。5.2 注入时出错No qualifying bean of type ‘javax.sql.DataSource‘ available错误信息No qualifying bean of type javax.sql.DataSource available: expected single matching bean but found 2: primaryDataSource,secondaryDataSource原因与解决 这个错误发生在你尝试注入DataSource类型但没有使用Qualifier指定具体Bean名称同时也没有任何一个DataSourceBean被标记为Primary。解决方法是二选一为其中一个DataSourceBean通常是主库添加Primary注解。在注入点使用Qualifier(“beanName”)明确指定要注入哪个Bean。5.3 MyBatis Mapper扫描失败或绑定到错误的数据源现象 Mapper接口无法被注入或者注入后执行SQL时报错连接了错误的数据库。排查步骤检查包路径确认你的Mapper接口所在的包是否被对应的MapperScan(basePackages “…”)注解所覆盖。路径必须完全匹配。检查sqlSessionFactoryRef确认MapperScan注解中的sqlSessionFactoryRef属性值与对应数据源配置类中SqlSessionFactoryBean的名称是否一致。检查XML文件位置确认SqlSessionFactoryBean的setMapperLocations方法指定的路径是否包含了该数据源对应的所有Mapper XML文件。可以使用通配符如classpath*:mapper/primary/**/*.xml。查看启动日志Spring Boot启动时会打印出MyBatis扫描和注册的Mapper列表。仔细查看日志确认你的Mapper是否被成功注册以及绑定到了哪个SqlSessionFactory上。5.4 事务不生效或跨数据源事务问题现象在使用了Transactional的方法中对从库非Primary数据源的操作没有回滚或者期望在同一个事务中的多个数据库操作实际上被分到了不同的事务中。排查与解决指定transactionManager确保你的Transactional注解中transactionManager属性指定了正确的事务管理器Bean名称。例如操作从库的方法应该使用Transactional(transactionManager “secondaryTransactionManager”)。理解事务边界Spring的声明式事务是基于AOP代理实现的。在同一个类内部的方法调用例如methodA()调用this.methodB()methodB()上的Transactional注解是不会生效的因为代理对象没有介入。需要通过注入自身代理或重构代码来解决。放弃跨库事务如前所述标准的Transactional无法管理跨不同DataSourceTransactionManager的事务。如果业务必须要求强一致性需要引入分布式事务框架否则考虑使用补偿性事务或最终一致性方案。5.5 连接池配置不生效现象在application.yml中为某个数据源配置的HikariCP参数如maximum-pool-size没有起作用。排查检查前缀确保配置的前缀与ConfigurationProperties(prefix “…”)中定义的前缀完全一致包括大小写和中间的连接符.或-。检查属性名HikariCP的属性名是特定的。例如连接地址是jdbc-url而不是url这是为了兼容性最大连接池大小是maximum-pool-size。最好参考官方文档或HikariConfig类的属性。检查配置类确保你的DataSourceBean是通过DataSourceBuilder.create().type(HikariDataSource.class).build()创建的这样ConfigurationProperties绑定的Hikari属性才会被应用到HikariDataSource实例上。配置多数据源是Spring Boot应用迈向实战的必修课。它要求开发者对Spring的Bean管理、依赖注入有更清晰的认识。从手动配置开始虽然步骤稍多但每一步都让你对底层机制有更牢固的把握。当你能熟练地配置和切换多个数据源时意味着你已经具备了处理更复杂数据层架构的能力。在实际项目中如果觉得手动配置繁琐再考虑引入dynamic-datasource-spring-boot-starter这类工具来提升开发效率但那时你就能更深刻地理解它背后替你做了哪些事情。