恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot多数据源配置实战:从手动配置到dynamic-datasource
首页
资讯中心
/
SpringBoot多数据源配置实战:从手动配置到dynamic-datasource
SpringBoot多数据源配置实战:从手动配置到dynamic-datasource
发布时间:2026/8/26 6:16:15
1. 项目背景与核心诉求在真实的业务开发中一个SpringBoot应用只连接一个数据库的场景往往只存在于教科书或者最简单的Demo里。随着业务模块的拆分、历史系统的整合或者读写分离、分库分表等架构演进一个服务需要同时与多个数据库打交道就成了家常便饭。比如你可能需要从主业务库MySQL读取订单数据同时又要去另一个独立的用户中心库PostgreSQL查询用户信息甚至还需要连接一个专门用于报表的ClickHouse进行分析查询。这时候单一数据源的配置就完全不够用了。SpringBoot的自动配置Auto-Configuration为我们提供了极大的便利spring-boot-starter-data-jpa或spring-boot-starter-data-jdbc会默认帮我们创建一个名为dataSource的Bean。但这也带来了一个“甜蜜的烦恼”当你在配置文件中定义了多个数据库连接信息时SpringBoot会感到困惑它不知道应该用哪一个来创建这个默认的dataSource从而抛出经典的错误Failed to configure a DataSource: url attribute is not specified and no embedded datasource could be configured.。这个错误信息直白地告诉你我找不到一个明确的数据源来用。因此配置多数据源的核心本质上就是手动接管数据源的创建过程明确地告诉Spring容器“这里有几个不同的数据源它们各自叫什么名字分别对应哪些配置以及在不同的场景下应该使用哪一个。” 这个过程需要我们跳出“全自动”的舒适区深入到Spring的Bean管理层面进行精细化的手动配置。这不仅是解决连接问题更是为后续复杂的数据访问场景如动态数据源路由打下坚实的基础。2. 多数据源配置的核心原理与设计思路在深入代码之前我们必须理解其背后的设计哲学。Spring框架管理Bean的核心是IoC容器数据源DataSource本身也是一个Bean。多数据源配置就是向容器中注册多个DataSource Bean并确保它们互不冲突各司其职。2.1 打破自动配置的“魔法”SpringBoot的自动配置基于条件注解如ConditionalOnMissingBean。以数据源为例当你的应用Classpath下存在相关依赖且你没有显式定义任何DataSourceBean时自动配置才会生效创建一个默认的。我们的策略就是抢先一步在自动配置触发之前手动创建出我们需要的多个DataSourceBean。一旦容器中有了我们定义的BeanSpringBoot的默认配置就会因为条件不满足而自动退出从而避免了冲突。2.2 配置分离与命名策略这是实现清晰架构的关键。我们不应该把所有数据库的配置都混在spring.datasource这个前缀下。最佳实践是为每个数据源定义独立的配置前缀。例如主库Primaryspring.datasource.primary从库或业务库Secondaryspring.datasource.secondary报表库Reportspring.datasource.report在application.yml或application.properties中它们看起来是这样的spring: datasource: primary: url: jdbc:mysql://localhost:3306/primary_db?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver secondary: url: jdbc:postgresql://localhost:5432/secondary_db username: postgres password: postgres driver-class-name: org.postgresql.Driver这种结构一目了然便于维护和扩展。2.3 数据源、事务管理器与持久化框架的绑定仅仅创建多个DataSourceBean是不够的。为了让我们的数据访问层如JPA的EntityManager、MyBatis的SqlSessionFactory知道在操作哪个数据库时使用哪个数据源我们需要为每个数据源配套创建一套完整的“执行环境”。这通常包括DataSource Bean连接池本身如HikariCP、Druid。平台事务管理器PlatformTransactionManager 管理数据库事务。对于JPA是JpaTransactionManager对于JDBC或MyBatis通常是DataSourceTransactionManager。必须为每个数据源单独配置否则事务将无法正确绑定到对应的数据源上。EntityManagerFactory / SqlSessionFactory Bean 这是数据访问操作的实际执行工厂。我们需要在创建它时显式地注入对应的DataSource和TransactionManager。整个设计思路可以概括为为每个逻辑上独立的数据源配置一套独立的、闭环的“数据访问组件链”。它们通过Bean的名称进行区分和引用。3. 基于Java Config的纯手动配置方案这是最经典、最灵活也是理解原理的最佳方式。我们以配置一个主库MySQL和一个从库PostgreSQL为例并使用JPA作为持久层框架。3.1 项目依赖准备首先在pom.xml中引入必要的依赖。注意我们不再需要spring-boot-starter-data-jpa的自动配置为我们创建单一数据源但依然需要它的JPA核心功能。dependencies !-- Spring Boot Starter Web (如果项目是Web应用) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JPA Starter (提供JPA规范支持如EntityManager) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- PostgreSQL 驱动 -- dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId scoperuntime/scope /dependency !-- 连接池 (Spring Boot 2.x 默认使用HikariCP可显式引入或使用Druid) -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /dependency !-- 或者使用阿里巴巴Druid连接池 -- !-- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.16/version /dependency -- /dependencies3.2 配置文件定义在application.yml中按照前述的命名策略进行配置spring: datasource: primary: jdbc-url: jdbc:mysql://127.0.0.1:3306/primary_db?useSSLfalseserverTimezoneUTC username: root password: your_mysql_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5 secondary: jdbc-url: jdbc:postgresql://127.0.0.1:5432/secondary_db username: postgres password: your_postgres_password driver-class-name: org.postgresql.Driver hikari: connection-timeout: 30000 maximum-pool-size: 15 minimum-idle: 3 jpa: # JPA通用配置注意hibernate.ddl-auto 等配置需要在每个EntityManagerFactory中单独设置 show-sql: true properties: hibernate: format_sql: true注意使用HikariCP时url属性需要写为jdbc-url否则可能会因属性名冲突导致配置不生效。这是一个常见的坑。3.3 Java配置类实现接下来是核心部分我们将创建两个配置类也可以合并到一个类中但分开更清晰分别定义主库和从库的完整组件链。PrimaryDataSourceConfig.java (主库配置)Configuration EnableTransactionManagement // 启用注解式事务管理 EnableJpaRepositories( basePackages com.yourproject.repository.primary, // 指定主库Repository的扫描包 entityManagerFactoryRef primaryEntityManagerFactory, transactionManagerRef primaryTransactionManager ) public class PrimaryDataSourceConfig { Primary // 关键注解指定该Bean为主要候选者当有多个同类型Bean时优先注入 Bean(name primaryDataSource) ConfigurationProperties(prefix spring.datasource.primary) // 绑定配置前缀 public DataSource primaryDataSource() { // Spring Boot会自动根据配置创建HikariDataSource return DataSourceBuilder.create().build(); } Primary Bean(name primaryEntityManagerFactory) public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory( EntityManagerFactoryBuilder builder, Qualifier(primaryDataSource) DataSource dataSource) { // 注入名为primaryDataSource的Bean MapString, Object properties new HashMap(); properties.put(hibernate.hbm2ddl.auto, update); // 主库的DDL策略 properties.put(hibernate.dialect, org.hibernate.dialect.MySQL8Dialect); // 方言 return builder .dataSource(dataSource) .packages(com.yourproject.entity.primary) // 指定主库实体类的包路径 .persistenceUnit(primaryPersistenceUnit) .properties(properties) .build(); } Primary Bean(name primaryTransactionManager) public PlatformTransactionManager primaryTransactionManager( Qualifier(primaryEntityManagerFactory) LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory) { return new JpaTransactionManager(primaryEntityManagerFactory.getObject()); } }SecondaryDataSourceConfig.java (从库配置)Configuration EnableTransactionManagement EnableJpaRepositories( basePackages com.yourproject.repository.secondary, entityManagerFactoryRef secondaryEntityManagerFactory, transactionManagerRef secondaryTransactionManager ) public class SecondaryDataSourceConfig { Bean(name secondaryDataSource) ConfigurationProperties(prefix spring.datasource.secondary) public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } Bean(name secondaryEntityManagerFactory) public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory( EntityManagerFactoryBuilder builder, Qualifier(secondaryDataSource) DataSource dataSource) { MapString, Object properties new HashMap(); properties.put(hibernate.hbm2ddl.auto, validate); // 从库采用验证模式避免误修改 properties.put(hibernate.dialect, org.hibernate.dialect.PostgreSQLDialect); return builder .dataSource(dataSource) .packages(com.yourproject.entity.secondary) .persistenceUnit(secondaryPersistenceUnit) .properties(properties) .build(); } Bean(name secondaryTransactionManager) public PlatformTransactionManager secondaryTransactionManager( Qualifier(secondaryEntityManagerFactory) LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory) { return new JpaTransactionManager(secondaryEntityManagerFactory.getObject()); } }3.4 实体与Repository层隔离配置完成后代码层面的隔离同样重要。你必须将不同数据源对应的实体Entity和仓库接口Repository放在不同的包下这与配置类中的packages和basePackages设置必须严格对应。src/main/java/com/yourproject/ ├── entity/ │ ├── primary/ │ │ └── PrimaryUser.java // 对应 primary_db 库中的表 │ └── secondary/ │ └── SecondaryLog.java // 对应 secondary_db 库中的表 └── repository/ ├── primary/ │ └── PrimaryUserRepository.java // 扫描包com.yourproject.repository.primary └── secondary/ └── SecondaryLogRepository.java // 扫描包com.yourproject.repository.secondaryPrimaryUserRepository.javaRepository public interface PrimaryUserRepository extends JpaRepositoryPrimaryUser, Long { // 这里的方法会自动使用 primaryEntityManagerFactory 和 primaryTransactionManager }SecondaryLogRepository.javaRepository public interface SecondaryLogRepository extends JpaRepositorySecondaryLog, Long { // 这里的方法会自动使用 secondaryEntityManagerFactory 和 secondaryTransactionManager }3.5 在Service层中使用在Service中你可以像使用普通的Repository一样注入它们Spring会根据注入点的类型和Qualifier如果有自动选择正确的Bean。由于我们为主要的组件DataSource, EntityManagerFactory, TransactionManager都加上了Primary注解所以在没有明确指定时会默认使用主库的组件。而对于从库的Repository因为其接口定义在com.yourproject.repository.secondary包下EnableJpaRepositories注解已经将其绑定到了从库的事务管理器。Service public class BusinessService { Autowired private PrimaryUserRepository primaryUserRepository; // 自动使用主库 Autowired private SecondaryLogRepository secondaryLogRepository; // 自动使用从库 Transactional(transactionManager primaryTransactionManager) // 可显式指定事务管理器 public void createUserAndLog() { PrimaryUser user new PrimaryUser(); user.setName(Test User); primaryUserRepository.save(user); // 这个方法上的事务注解如果不指定会根据Repository的配置自动选择。 // 但这里涉及两个库需要特别注意事务边界通常跨库事务需要分布式事务解决方案如Seata // 简单场景下可以考虑最终一致性。 SecondaryLog log new SecondaryLog(); log.setAction(CREATE_USER); log.setUserId(user.getId()); secondaryLogRepository.save(log); } }4. 使用dynamic-datasource-spring-boot-starter简化配置手动配置虽然灵活但步骤繁琐尤其是在数据源数量较多时。国内开源社区有一款非常流行的组件dynamic-datasource-spring-boot-starter它极大地简化了多数据源和动态数据源的配置。其核心思想是约定大于配置通过一个抽象的动态数据源DynamicRoutingDataSource来路由所有数据库操作。4.1 快速集成步骤第一步添加依赖dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.6.1/version !-- 请使用最新稳定版 -- /dependency !-- 你的数据库驱动如MySQL -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency第二步配置文件在application.yml中配置格式发生了根本变化spring: datasource: dynamic: primary: master # 设置默认的数据源主数据源默认值为master strict: false # 是否严格匹配数据源默认false。true时未匹配到指定数据源会抛异常false则使用默认数据源 datasource: master: # 数据源名称可以自定义这里用master表示主库 url: jdbc:mysql://localhost:3306/master_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave_1: # 第二个数据源名称slave_1 url: jdbc:mysql://localhost:3306/slave_1_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver log: # 第三个数据源可以是其他类型数据库如PostgreSQL url: jdbc:postgresql://localhost:5432/log_db username: postgres password: postgres driver-class-name: org.postgresql.Driver第三步使用DS注解切换数据源这是该组件最精髓的部分。你无需再配置多个EntityManagerFactory和TransactionManager。只需要在需要切换数据源的地方Mapper/Repository、Service方法或类上添加DS注解。Service public class UserService { // 1. 在Mapper/Repository接口上使用 Repository DS(master) // 指定该接口所有方法使用master数据源 public interface UserMapper extends BaseMapperUser { } Repository DS(slave_1) // 指定该接口所有方法使用slave_1数据源 public interface LogMapper extends BaseMapperLog { } Autowired private UserMapper userMapper; // 操作master库 Autowired private LogMapper logMapper; // 操作slave_1库 // 2. 在Service方法上使用优先级高于类上的注解 DS(slave_1) public User getUserFromSlave(Long id) { return userMapper.selectById(id); // 这个方法内userMapper也会被路由到slave_1 // 注意这里有个坑userMapper接口上定义了DS(master)但当前方法注解是DS(slave_1)。 // dynamic-datasource的规则是方法注解 类注解 接口注解。 // 但这里的userMapper是一个Bean其代理对象在调用时会先看当前线程上下文的数据源是什么。 // 更安全的做法是为操作不同数据源的实体定义不同的Mapper接口并分别标注DS。 } // 3. 在Service类上使用该类所有方法默认使用该数据源 Service DS(log) public class LogService { Autowired private LogMapper logMapper; // 由于LogMapper本身有DS(slave_1)但LogService类有DS(log)方法调用时会用哪个 // 实际测试表明对于Mapper/Repository的DS注解其优先级很高。通常建议保持一致性。 // 最佳实践让Mapper的DS注解作为数据源选择的最终决定者Service层的DS用于那些没有特定Mapper的JDBC操作或统一切换。 } // 4. 无注解时使用默认数据源即配置中的primary: master public User getDefaultUser(Long id) { return userMapper.selectById(id); // 使用userMapper的DS(master) } }4.2 核心机制与避坑指南原理该组件会向Spring容器注册一个DynamicRoutingDataSource它内部维护了一个数据源Map。通过AOP拦截被DS注解或手动设置的方法在调用前将当前线程要使用的数据源Key如”master”设置到线程上下文中DynamicRoutingDataSource的getConnection()方法会根据这个Key去Map里获取真实的数据源。事务管理组件自动集成了Spring的事务管理。关键点在于Transactional和DS注解必须加在同一个方法上并且DS需要放在Transactional之内否则数据源切换可能失效导致事务内跨库。// 正确做法 DS(slave_1) Transactional(rollbackFor Exception.class) public void updateInSlave() { // ... } // 错误做法事务注解在外层调用方法DS注解在内部私有方法切换可能失败。 Transactional public void outerMethod() { innerMethod(); } DS(slave_1) private void innerMethod() { // AOP可能无法拦截私有方法且事务上下文已确定数据源 // ... }多数据源事务和手动配置一样跨多个物理数据库的写操作无法通过本地事务保证一致性。dynamic-datasource同样不提供分布式事务支持需要借助Seata等框架。配置项strict当strict: true时如果指定的数据源不存在比如DS(not_exist)会直接抛出异常。当strict: false时则会回退到使用primary指定的默认数据源。生产环境建议设为true及早暴露配置错误。性能与连接池每个定义的数据源都会独立初始化一个连接池默认使用HikariCP。务必根据每个数据源的实际压力在配置中调整hikari或druid的连接池参数避免资源浪费或不足。5. 生产环境下的进阶考量与最佳实践将多数据源配置应用到生产环境远不止让程序跑起来那么简单。以下几个方面的考量至关重要。5.1 连接池选型与精细化配置无论是手动配置还是使用starter连接池都是影响性能和数据源稳定性的基石。HikariCP以其高性能和简单可靠成为Spring Boot默认选择而Druid则提供了强大的监控和防御功能。HikariCP配置示例在对应数据源配置下spring: datasource: master: url: ... username: ... password: ... driver-class-name: ... hikari: connection-timeout: 30000 # 连接超时时间毫秒 maximum-pool-size: 20 # 最大连接数根据数据库和服务负载调整 minimum-idle: 5 # 最小空闲连接数 idle-timeout: 600000 # 空闲连接存活时间毫秒 max-lifetime: 1800000 # 连接最大生命周期毫秒 connection-test-query: SELECT 1 # 连接测试查询MySQL推荐 # 或者使用 validation-timeout 和 connection-init-sqlDruid配置示例如果你选择Druid配置会更丰富包括监控、统计、防御SQL注入等。spring: datasource: master: url: ... username: ... password: ... driver-class-name: ... type: com.alibaba.druid.pool.DruidDataSource # 指定类型 druid: initial-size: 5 min-idle: 5 max-active: 20 # ... 其他大量监控和过滤器配置经验之谈对于大多数Web应用HikariCP的默认配置已经足够优秀。如果你需要详细的监控信息如SQL执行次数、慢查询、连接堆栈Druid是更好的选择。切忌盲目调大maximum-pool-size过大的连接池会导致数据库负载过高性能反而下降。一个常用的估算公式是连接数 ((核心数 * 2) 有效磁盘数)。例如4核服务器可以设置为 10 左右作为起点进行压测调整。5.2 读写分离场景下的动态路由多数据源的一个典型应用是读写分离写操作走主库Master读操作走一个或多个从库Slave。使用dynamic-datasource-spring-boot-starter可以较容易地实现。配置spring: datasource: dynamic: primary: master datasource: master: url: jdbc:mysql://master-host:3306/db # ... slave_1: url: jdbc:mysql://slave1-host:3306/db # ... slave_2: url: jdbc:mysql://slave2-host:3306/db # ...使用在Service方法上通过DS注解手动指定。Service public class OrderService { Autowired private OrderMapper orderMapper; DS(master) // 写操作用主库 Transactional public void createOrder(Order order) { orderMapper.insert(order); } DS(slave_1) // 读操作用从库1 public Order getOrder(Long id) { return orderMapper.selectById(id); } // 更高级的用法可以结合AOP根据方法名前缀如get, find, query自动切换到从库 }对于更复杂的负载均衡如随机、轮询选择从库dynamic-datasource支持数据源组和负载均衡策略配置可以实现一定程度的自动化路由。5.3 多数据源下的健康检查与监控Spring Boot Actuator的/health端点默认会检查名为dataSource的Bean。在多数据源情况下你需要自定义健康指示器来检查所有数据源。手动配置场景你可以为每个DataSourceBean创建独立的DataSourceHealthIndicator。Configuration public class DataSourceHealthConfig { Bean public HealthIndicator primaryDataSourceHealth(Qualifier(primaryDataSource) DataSource dataSource) { return new DataSourceHealthIndicator(dataSource); } Bean public HealthIndicator secondaryDataSourceHealth(Qualifier(secondaryDataSource) DataSource dataSource) { return new DataSourceHealthIndicator(dataSource); } }这样访问/actuator/health时就能看到每个数据源的健康状态。使用dynamic-datasource场景该starter会自动为每个数据源注册健康指示器并在/actuator/health中展示详细信息非常方便。5.4 常见问题排查“踩坑”实录Failed to configure a DataSource: url attribute is not specified问题这是最经典的错误。根本原因是Spring Boot的自动配置在类路径下发现了JDBC相关依赖但在你的配置中尤其是手动配置时没有定义一个能被它识别的默认数据源。解决方案A推荐在启动类上排除DataSourceAutoConfiguration。这明确告诉Spring Boot“不要自动配置数据源我全都要自己来”。SpringBootApplication(exclude {DataSourceAutoConfiguration.class}) public class YourApplication { ... }方案B确保你的配置文件中spring.datasource.url这个属性存在且有效如果你打算保留一个默认数据源。或者在手动配置的某个DataSourceBean上加上Primary注解。事务不生效或数据源切换失败问题在使用了Transactional的方法中数据源没有按预期切换。根因Spring事务的AOP拦截器在方法执行前就确定了Connection即数据源。如果DS注解的切面在事务切面之后执行那么数据源切换就晚了。解决确保DS注解和Transactional注解作用于同一个方法。确保DS注解的切面优先级高于Transactional。dynamic-datasource-spring-boot-starter已经处理了这一点。如果是手动配置需要关注AOP的执行顺序。JPA的ddl-auto在多个EntityManagerFactory下行为异常问题配置了多个LocalContainerEntityManagerFactoryBean后发现表的创建/更新没有按预期发生在对应的数据库。根因JPA的全局配置spring.jpa.hibernate.ddl-auto可能被所有EntityManagerFactory继承或者配置没有正确应用到每个工厂。解决不要在application.yml的spring.jpa下设置ddl-auto。像我们在3.3节中做的那样在每个LocalContainerEntityManagerFactoryBean的构建过程中通过properties()方法单独、显式地设置Hibernate属性包括hibernate.dialect和hibernate.hbm2ddl.auto。这样能做到精准控制。多数据源导致Flyway/Liquibase迁移脚本混乱问题使用了数据库版本管理工具但脚本在多个数据库上重复执行或错乱执行。解决Flyway和Liquibase都支持多数据源但需要单独配置。你需要为每个数据源定义独立的Flyway或LiquibaseBean并指定各自的DataSource、locations脚本路径。核心思路同样是“各管各的”。以Flyway手动配置为例Bean Primary public Flyway primaryFlyway(Qualifier(primaryDataSource) DataSource dataSource) { return Flyway.configure() .dataSource(dataSource) .locations(classpath:db/migration/primary) // 主库脚本路径 .load(); } Bean public Flyway secondaryFlyway(Qualifier(secondaryDataSource) DataSource dataSource) { return Flyway.configure() .dataSource(dataSource) .locations(classpath:db/migration/secondary) // 从库脚本路径 .load(); }并在应用启动时按顺序执行这些Bean的migrate()方法。