SpringBoot多数据源配置实战:从手动配置到dynamic-datasource组件详解
2026/8/26 6:17:49 网站建设 项目流程

1. 项目概述:为什么需要多数据源?

在真实的业务开发里,一个SpringBoot应用只连一个数据库的场景,反而更像是“学生作业”或者“演示Demo”。稍微有点规模的项目,数据来源往往不止一个。比如,你的核心业务数据放在MySQL主库,用户行为日志为了高性能写入可能放在另一个MySQL实例,而一些报表查询又需要从专门的只读从库或者像PostgreSQL这样的分析型数据库里拉数据。更常见的场景是,公司内部系统整合,你需要同时连接一个自研的业务库和一个采购的第三方系统数据库。这时候,如果还死守着单数据源的配置,要么代码里到处写死JDBC连接,要么就得搞出一堆别扭的“曲线救国”方案,维护起来简直是噩梦。

所以,掌握SpringBoot配置多数据源,不是一个“炫技”的选修课,而是一个合格后端开发者必须面对的“生存技能”。它直接关系到你项目的架构清晰度、代码可维护性以及未来面对复杂数据需求时的扩展能力。网上教程很多,但要么讲得太浅,只告诉你怎么配application.yml;要么讲得太深,一上来就分析AbstractRoutingDataSource的源码,让新手望而却步。这篇内容,我会从一个老鸟的实战视角,带你从“是什么”、“为什么”到“怎么做”,把配置多数据源这件事掰开揉碎了讲清楚,重点不是让你照抄配置,而是理解每一步背后的设计意图和可能踩的坑。

2. 核心思路与方案选型:不止一种玩法

在动手写代码之前,我们得先搞清楚有哪几条路可以走。不同的方案适用于不同的场景和复杂度,选错了后期重构的成本可不低。

2.1 方案一:基于配置类的显式声明(推荐新手入门)

这是最直观、也是最容易理解的方式。核心思想就是:放弃SpringBoot的自动配置,我们自己手动创建多个DataSourceSqlSessionFactoryTransactionManager等Bean

为什么推荐新手从这里开始?因为这种方式将每个数据源的“生命周期”完全掌控在自己手里。从数据源创建、事务管理器绑定,到MyBatis的会话工厂,每一步你都看得见摸得着。它虽然代码量稍多,但逻辑极其清晰,排错也方便。你能够非常明确地知道UserDao用的是哪个数据源,LogDao又绑定了哪个事务管理器。当配置出错时,你很容易定位到是primaryDataSource这个Bean没创建成功,还是secondaryTransactionManager注入错了。

它的工作原理是什么?简单说,就是利用Spring的@Configuration配置类,定义两套(或多套)完整的、彼此独立的JPA/MyBatis数据访问组件。然后通过@Bean注解给它们起不同的名字,或者使用@Qualifier注解在注入时进行区分。SpringBoot默认的DataSourceAutoConfiguration会因为我们手动定义了DataSourceBean而自动退出,不会和我们“打架”。

适用场景:

  • 数据源数量固定且较少(比如2-3个)。
  • 不同数据源访问的数据库类型可能不同(如一个MySQL,一个PostgreSQL)。
  • 团队对Spring Boot自动配置原理还不算特别熟悉,追求稳定和可控。

2.2 方案二:使用dynamic-datasource-spring-boot-starter(推荐生产级项目)

这是一个非常流行的开源组件,作者是国内的开发者。它的核心思想是:引入一个“动态数据源”,并通过AOP在方法执行前,根据注解(如@DS(“slave”))来动态切换当前线程使用的数据源

为什么它在生产环境更受青睐?因为它极大地简化了代码。你不需要为每个数据源写一堆重复的配置类,只需要在application.yml里定义好所有数据源,然后在Service层的方法上打一个@DS注解就行了。它内部通过AbstractRoutingDataSource和Spring AOP实现了数据源的动态路由,对业务代码的侵入性非常小。这对于有大量读写分离、分库分表(简单场景)需求的项目来说,简直是神器。

需要注意的坑:这个组件虽好,但也不是银弹。我踩过最大的一个坑就是事务管理。如果你在同一个@Transactional注解的方法内,调用了多个带有不同@DS注解的方法,那么数据源切换可能会失效,因为事务管理器通常是在方法入口处就确定了数据源。组件官方文档对此有详细说明,通常的解决方案是避免在事务方法内跨数据源调用,或者使用其提供的事务增强特性。另一个常见错误是配置问题,比如在spring.datasource.dynamic.primary没设置对,或者数据源名称在注解里写错了,启动时就会报类似dynamic-datasource failed to configure a datasource: 'url' attribute的错误,其实就是在说“老大,你让我用的那个数据源,我找不到它的连接信息啊”。

适用场景:

  • 数据源数量较多,或可能动态增加。
  • 有明确的读写分离需求。
  • 希望业务代码保持简洁,通过注解控制数据源。
  • 团队愿意引入并学习一个第三方组件。

2.3 方案三:完全手写AbstractRoutingDataSource(高阶定制)

这个方案是方案二的“手动版”。你需要自己继承AbstractRoutingDataSource,实现determineCurrentLookupKey()方法,返回当前线程应该使用的数据源标识(key)。然后,你需要自己用AOP或者HandlerInterceptor在请求的某个环节(比如根据请求头、线程变量)来设置这个key。

什么情况下你会需要自己手写?当你的数据源路由逻辑非常复杂,超出了@DS注解的能力范围。比如,你的数据源选择不是基于方法,而是基于登录用户的租户ID(多租户系统),或者基于某个复杂的业务规则计算出来的分片键。这时候,你就需要把路由逻辑牢牢抓在自己手里。

不推荐新手直接尝试的原因:这个方案需要对Spring的AOP、事务管理、线程上下文(ThreadLocal)有比较深的理解。你需要自己处理好数据源切换的时机,以及最关键的事务上下文传播问题,否则极易出现数据源混乱或连接泄露。它更偏向于一种“框架级”的定制,适用于有特殊架构需求的场景。

我的选择建议:对于绝大多数业务项目,我强烈推荐方案二。它平衡了易用性、功能和社区支持。本篇文章的后续实操部分,也将以方案一(显式配置)方案二(动态数据源组件)作为重点,因为这两个覆盖了90%以上的应用场景。方案三作为知识拓展,大家了解其思想即可。

3. 方案一实操:手动配置多数据源(MyBatis版)

我们假设一个经典场景:应用需要连接两个MySQL数据库,一个叫primary_db(主库,负责核心业务读写),一个叫report_db(报表库,只读)。

3.1 项目结构与依赖准备

首先,创建一个标准的SpringBoot项目。pom.xml中需要的基础依赖如下:

<dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 我们使用MyBatis,这是它的SpringBoot官方整合包 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> <!-- 请使用最新稳定版 --> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 其他工具依赖,如Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

关键点:这里我们引入了mybatis-spring-boot-starter,它会自动配置单数据源下的MyBatis。但当我们手动配置多数据源时,它的部分自动配置会失效,这正是我们想要的——我们需要夺回控制权。

3.2 配置文件拆分与定义

我们不把所有配置堆在application.yml里,那样会显得很乱。采用多配置文件的方式更清晰。

application.yml(主配置,设置激活的profile和公共属性)

spring: profiles: active: multi-ds # 激活名为 multi-ds 的配置 # 其他全局配置,比如服务器端口 server: port: 8080

application-multi-ds.yml(多数据源专属配置)

# 第一个数据源:主库 primary: datasource: url: jdbc:mysql://localhost:3306/primary_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 连接池配置,这里使用HikariCP,SpringBoot默认 hikari: pool-name: PrimaryHikariPool maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 # 第二个数据源:报表库 secondary: datasource: url: jdbc:mysql://localhost:3307/report_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: report_user password: report_pass driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: SecondaryHikariPool maximum-pool-size: 10 # 报表库通常压力小,连接数可以设少点 minimum-idle: 2 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 # MyBatis的公共配置,比如别名、mapper位置(这里配置的会被两个数据源共享基础设置,但各自工厂会覆盖) mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发时开启,方便看SQL

注意:这里我们把数据源配置的前缀从默认的spring.datasource改成了primary.datasourcesecondary.datasource。这是关键一步,目的是让SpringBoot的DataSourceAutoConfiguration无法识别这些配置,从而避免它自动创建单数据源Bean。我们自己会在配置类里读取这些前缀的属性。

3.3 主数据源(Primary)配置类

我们为primary_db创建一套完整的访问组件。

package com.example.demo.config.primary; import com.zaxxer.hikari.HikariDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; // 重要注解! import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; @Configuration // 指定这个数据源对应的MyBatis Mapper接口所在的包 @MapperScan( basePackages = "com.example.demo.mapper.primary", sqlSessionFactoryRef = "primarySqlSessionFactory" ) public class PrimaryDataSourceConfig { /** * 创建主数据源Bean。 * @ConfigurationProperties 会读取 `primary.datasource` 开头的配置,并注入到HikariDataSource的属性中。 * @Primary 注解是关键!它告诉Spring,当有多个同类型(DataSource)的Bean时,优先使用这个。 * 这确保了那些没有指定名字的自动注入(比如JdbcTemplate)会用到这个主数据源。 */ @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "primary.datasource") @Primary public DataSource primaryDataSource() { // 使用Spring Boot的Builder模式创建,它会自动根据配置绑定Hikari属性 return DataSourceBuilder.create().type(HikariDataSource.class).build(); } /** * 创建主数据源的事务管理器Bean。 * 事务管理器需要知道它管理的是哪个数据源,所以通过@Qualifier指定我们上面创建的primaryDataSource。 */ @Bean(name = "primaryTransactionManager") @Primary public DataSourceTransactionManager primaryTransactionManager( @Qualifier("primaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } /** * 创建主数据源的MyBatis SqlSessionFactory。 * 这是MyBatis的核心,负责创建SqlSession。我们需要为它设置具体的数据源和Mapper XML文件的位置。 */ @Bean(name = "primarySqlSessionFactory") @Primary public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); // 设置数据源 sessionFactoryBean.setDataSource(dataSource); // 设置Mapper XML文件的位置。这里我们约定主数据源的Mapper XML放在 classpath:mapper/primary/ 下 sessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mapper/primary/*.xml") ); // 如果你有全局的MyBatis配置(比如在application.yml里定义的),也可以在这里设置 // sessionFactoryBean.setConfiguration(mybatisConfiguration); return sessionFactoryBean.getObject(); } }

逐行解析与避坑指南:

  1. @Primary注解:这是多数据源配置的灵魂。Spring容器里如果有多个DataSourceTransactionManager,在自动注入时就会困惑。@Primary标记了“默认选项”。通常,你业务中最主要、最常用的那个数据源应该被标记为@Primary。这样,像JdbcTemplate这种没有指定@Qualifier的Bean,就会自动使用主数据源,避免报NoUniqueBeanDefinitionException错误。

  2. @MapperScan注解:它的basePackages属性指定了这个SqlSessionFactory负责扫描的Mapper接口包。sqlSessionFactoryRef属性则明确绑定到我们下面定义的primarySqlSessionFactoryBean。务必确保这两个属性对应正确,否则你的Mapper接口会被错误的SqlSessionFactory管理,导致连接错数据库。

  3. DataSourceBuilder.create().type(HikariDataSource.class).build():这里显式指定了使用HikariCP连接池。虽然SpringBoot 2.x默认就是Hikari,但显式声明可以避免因依赖或版本变化导致的意外。通过@ConfigurationProperties,配置文件里primary.datasource.hikari.*下的所有属性都会自动注入到这个HikariDataSource实例中。

  4. Mapper XML路径classpath:mapper/primary/*.xml。这是一种良好的实践,将不同数据源的SQL映射文件物理隔离到不同的目录,清晰且不易出错。你需要在resources目录下建立mapper/primary文件夹来存放对应的XML文件。

3.4 从数据源(Secondary)配置类

第二个数据源的配置类与主数据源几乎是对称的,但绝对不能再用@Primary注解。

package com.example.demo.config.secondary; import com.zaxxer.hikari.HikariDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; @Configuration // 注意:这里扫描的是 secondary 的Mapper包 @MapperScan( basePackages = "com.example.demo.mapper.secondary", sqlSessionFactoryRef = "secondarySqlSessionFactory" ) public class SecondaryDataSourceConfig { // Bean的名字不能重复! @Bean(name = "secondaryDataSource") @ConfigurationProperties(prefix = "secondary.datasource") public DataSource secondaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Bean(name = "secondaryTransactionManager") public DataSourceTransactionManager secondaryTransactionManager( @Qualifier("secondaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "secondarySqlSessionFactory") public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); // XML文件路径指向 secondary 目录 sessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mapper/secondary/*.xml") ); return sessionFactoryBean.getObject(); } }

关键区别:

  • 移除了所有@Primary注解。
  • 所有Bean的name(或value)属性都改为了secondaryXXX
  • @MapperScan扫描的包路径和sqlSessionFactoryRef指向了新的Bean。
  • @ConfigurationProperties前缀读取secondary.datasource
  • Mapper XML路径指向classpath:mapper/secondary/

3.5 编写Mapper与Service进行测试

现在,我们来创建对应的Mapper和Service,验证配置是否生效。

1. 实体类(简单示例)

package com.example.demo.entity.primary; import lombok.Data; @Data public class User { private Long id; private String name; private String email; }
package com.example.demo.entity.secondary; import lombok.Data; @Data public class Report { private Long id; private String reportName; private Integer viewCount; }

2. Primary数据源的Mapper接口和XML

package com.example.demo.mapper.primary; import com.example.demo.entity.primary.User; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper // 这里@Mapper可加可不加,因为@MapperScan已经扫描了这个包 public interface UserMapper { List<User> selectAllUsers(); }

resources/mapper/primary/UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.primary.UserMapper"> <select id="selectAllUsers" resultType="com.example.demo.entity.primary.User"> SELECT id, name, email FROM user </select> </mapper>

3. Secondary数据源的Mapper接口和XML

package com.example.demo.mapper.secondary; import com.example.demo.entity.secondary.Report; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface ReportMapper { List<Report> selectAllReports(); }

resources/mapper/secondary/ReportMapper.xml

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.secondary.ReportMapper"> <select id="selectAllReports" resultType="com.example.demo.entity.secondary.Report"> SELECT id, report_name, view_count FROM report </select> </mapper>

4. Service层调用

package com.example.demo.service; import com.example.demo.entity.primary.User; import com.example.demo.entity.secondary.Report; import com.example.demo.mapper.primary.UserMapper; import com.example.demo.mapper.secondary.ReportMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.sql.DataSource; import java.util.List; @Service public class DemoService { @Autowired private UserMapper userMapper; // 自动注入,由于UserMapper在primary的@MapperScan下,所以绑定primarySqlSessionFactory @Autowired private ReportMapper reportMapper; // 自动注入,绑定secondarySqlSessionFactory /** * 演示无事务的查询 */ public void queryFromMultipleSources() { List<User> users = userMapper.selectAllUsers(); System.out.println("主库用户数: " + users.size()); List<Report> reports = reportMapper.selectAllReports(); System.out.println("报表库报告数: " + reports.size()); } /** * 演示在主数据源上使用事务。 * 注意:@Transactional 默认使用标记了@Primary的TransactionManager,即primaryTransactionManager。 */ @Transactional public void updateUserWithTransaction(User user) { // 这里执行更新用户的SQL,会使用primaryDataSource的事务 // userMapper.update(user); System.out.println("在主库事务中更新用户"); } /** * 演示在从数据源上使用事务。 * 必须通过@Transactional的value或transactionManager属性指定具体的事务管理器Bean名称。 */ @Transactional(transactionManager = "secondaryTransactionManager") public void updateReportWithTransaction(Report report) { // 这里执行更新报告的SQL,会使用secondaryDataSource的事务 // reportMapper.update(report); System.out.println("在报表库事务中更新报告"); } /** * 演示注入特定的DataSource或JdbcTemplate。 * 如果你想直接使用JdbcTemplate操作某个特定的数据源,可以这样注入。 */ @Autowired @Qualifier("secondaryDataSource") // 指定注入名为secondaryDataSource的Bean private DataSource secondaryDataSource; // 或者直接注入一个针对secondaryDataSource的JdbcTemplate Bean(需要在配置类中定义) // @Autowired // @Qualifier("secondaryJdbcTemplate") // private JdbcTemplate secondaryJdbcTemplate; }

启动与测试:

  1. 确保你的primary_dbreport_db两个MySQL实例已启动,并且库表存在。
  2. 启动SpringBoot应用。观察控制台日志,应该能看到两个Hikari连接池分别初始化的信息。
  3. 调用DemoService.queryFromMultipleSources()方法,如果控制台分别打印出两个库的查询结果数量,恭喜你,多数据源配置成功了!

4. 方案二实操:使用dynamic-datasource-spring-boot-starter

手动配置虽然清晰,但每个数据源都要写一堆模板代码。对于追求效率的项目,我们上“全家桶”。

4.1 引入依赖与基础配置

首先,在pom.xml中添加依赖。请务必查看官方仓库使用最新版本。

<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>4.3.0</version> <!-- 示例版本,请使用最新 --> </dependency> <!-- 其他依赖如mybatis-plus-starter、web等照常 -->

然后,在application.yml中配置。这里和方案一有本质区别:我们又要用回spring.datasource.dynamic这个标准前缀了,因为组件要读取它。

spring: datasource: dynamic: primary: master # 设置默认的数据源(必须),默认值就是master strict: false # 是否严格匹配数据源,默认false。true时未匹配到指定数据源会报错,false则使用默认数据源 datasource: master: # 数据源名称,可以自定义,这里用master代表主库 url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 slave1: # 第二个数据源,名称slave1 url: jdbc:mysql://localhost:3307/slave_db?useSSL=false&serverTimezone=Asia/Shanghai username: slave_user password: slave_pass driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 15 # 你可以继续添加 slave2, oracle-db, postgres-db 等等

配置解读:

  • primary: 指定当没有显式使用数据源注解,或者注解指定的数据源找不到(且strict=false)时,默认使用的数据源名称。这里设为master
  • datasource节点下,每一个key(如masterslave1)就是一个数据源的名称,其下的配置和单数据源时完全一样。组件会自动根据这些配置创建对应的DataSourceBean。

4.2 在代码中使用@DS注解切换数据源

这是最核心、最便捷的部分。你几乎不需要额外的配置类。

1. 在Service或Mapper层的方法上使用@DS

package com.example.demo.service; import com.baomidou.dynamic.datasource.annotation.DS; import com.example.demo.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class DynamicDataSourceService { @Autowired private UserMapper userMapper; // 假设这个Mapper是使用MyBatis-Plus或普通MyBatis生成的 /** * 这个方法默认使用 @DS 注解指定的数据源。 * 如果类上没有@DS,方法上也没有@DS,则使用配置文件中的 `primary` 数据源(即master)。 */ @DS("slave1") // 指定这个方法使用名为 `slave1` 的数据源 public List<User> getUsersFromSlave() { return userMapper.selectList(null); // 假设使用MyBatis-Plus } /** * 这个方法没有@DS注解,将使用类上注解的数据源。 * 如果类上也没有,则回退到默认的 primary (master) 数据源。 */ public User getUserByIdFromMaster(Long id) { return userMapper.selectById(id); } /** * 在类级别注解,表示这个类里所有方法默认使用 `master` 数据源。 * 方法上的注解优先级高于类上的注解。 */ @DS("master") @Service public class MasterService { public void updateUser(User user) { // 默认在master数据源上执行 userMapper.updateById(user); } @DS("slave1") public User findUserFromSlave(Long id) { // 这个方法覆盖了类注解,使用slave1数据源 return userMapper.selectById(id); } } }

@DS注解的优先级规则(务必牢记):方法注解>类注解>默认数据源(primary)这意味着,你可以在类上定义一个常用的数据源,然后在某个特殊方法上覆盖它,非常灵活。

4.3 深入理解事务管理与@DSTransactional

这是使用dynamic-datasource最容易出问题的地方,必须单独拿出来讲。

问题场景:假设你有一个Service方法,它内部调用了两个DAO方法,一个需要写主库,一个只需要读从库。如果你在Service方法上使用了Spring原生的@Transactional,那么在整个事务范围内,数据源会被固定为事务开始时确定的那一个(通常是默认数据源或第一个被调用的@DS方法所在数据源),导致@DS注解失效,所有数据库操作都跑到同一个库去了。

解决方案:使用@DSTransactional组件提供了@DSTransactional注解来处理多数据源事务。但它不支持跨数据源的分布式事务(比如XA事务),它只能保证单个数据源内的事务性。它的作用是:确保在注解的方法内,所有数据库操作都在同一个数据源上执行,并在这个数据源上开启事务。

import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.dynamic.datasource.annotation.DSTransactional; @Service public class TransactionService { @DS("master") @DSTransactional // 这个注解确保下面所有操作在`master`数据源上,并开启事务 public void businessMethod() { // 操作1:更新master库的用户表 userMapper.update(...); // 操作2:在master库插入一条日志 logMapper.insert(...); // 这两个操作在同一个物理事务里,要么都成功,要么都回滚。 } @DS("slave1") @DSTransactional // 这个注解确保下面所有操作在`slave1`数据源上,并开启事务 public void readOnlyMethod() { // 复杂查询,可能需要多次查询slave1,并保证在同一个连接/事务视图内(可重复读) reportMapper.complexQuery1(...); reportMapper.complexQuery2(...); } }

重要限制与最佳实践:

  1. 严禁混用:在同一个方法内,不要同时使用@Transactional@DS。要么用@DSTransactional,要么就不要事务。
  2. 避免跨库事务@DSTransactional无法保证“更新A库和更新B库”同时成功或失败。如果你有强一致性要求,需要引入Seata等分布式事务中间件,或者从业务设计上避免跨库写操作(如最终一致性)。
  3. 只读事务:对于纯查询,可以使用@DS(“slave”)+@Transactional(readOnly = true)。但注意,这仍然会绑定到某个数据源的事务管理器上。对于简单的查询,不加任何事务注解往往是更轻量的选择。
  4. 传播行为@DSTransactional支持Spring的事务传播属性(如Propagation.REQUIRES_NEW),但行为是针对当前数据源的。

4.4 高级特性与配置

除了基本的@DS,该组件还提供了一些高级功能:

1. SPI扩展与自定义数据源选择你可以实现DynamicDataSourceStrategy接口,来自定义负载均衡策略(比如多个从库随机选、轮询)。也可以在determineCurrentLookupKey前后进行自定义逻辑,实现基于线程变量、请求参数等复杂路由。

2. 支持多种数据源类型在配置中,除了url方式,还支持jndi-namehikaridruid等多种连接池的直接配置。

3. 健康检查与监控组件会暴露数据源的健康指标(需要Actuator依赖),你可以在/actuator/health端点查看各个数据源的状态。

4. 配置分离对于敏感的生产环境密码,强烈建议将数据源配置放在spring.datasource.dynamic.datasource.master.password这样的属性中,并通过spring.config.import或Apollo/Nacos等配置中心引入,而不是硬编码在YAML文件里。

5. 常见问题、排查技巧与性能优化实录

配置多数据源的过程中,我踩过的坑不计其数。下面把这些血泪教训整理成表,希望能帮你快速排雷。

问题现象可能原因排查步骤与解决方案
启动报错:Failed to configure a DataSource: ‘url’ attribute is not specified1. SpringBoot自动配置试图创建单数据源,但没找到spring.datasource.url
2. 在方案一中,你可能忘了在@ConfigurationProperties中指定正确的前缀,或者配置项根本没加载。
1.方案一:检查你的配置类@ConfigurationProperties(prefix=“xxx”)中的prefix是否与application.yml中的配置前缀完全一致。检查配置文件是否被正确激活(spring.profiles.active)。
2.方案二:检查spring.datasource.dynamic.datasource下的每个数据源是否都正确配置了url。检查缩进是否正确(YAML对缩进敏感)。
启动报错:No qualifying bean of type ‘javax.sql.DataSource’ availableSpring容器中存在多个DataSource类型的Bean,但某个地方(如JdbcTemplateEntityManager)尝试自动注入时,没有用@Qualifier指定名字,Spring无法决定用哪个。1.方案一:确保你的主数据源Bean上标记了@Primary注解。
2. 检查是否有其他地方(比如第三方库)自动配置了DataSource。可以尝试在启动类上加@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})暂时排除自动配置,但这不是根本解法,需找到冲突源。
启动报错:Bean named ‘xxx’ is expected to be of type ‘SqlSessionFactory’ but was actually of type ‘…’@MapperScan注解的sqlSessionFactoryRef属性指向的Bean名称错误,或者该Bean根本没有被成功创建。1. 检查配置类中@Bean(name = “primarySqlSessionFactory”)等方法名是否与@MapperScan(sqlSessionFactoryRef = “primarySqlSessionFactory”)中的字符串完全一致。
2. 检查SqlSessionFactoryBeansetDataSource注入的DataSourceBean是否存在且名称正确。
@DS注解切换数据源无效,所有操作都跑到默认库1.事务问题:方法被Spring的@Transactional注解包裹,导致数据源在事务开始时就被固定。
2.注解位置错误@DS注解在了Controller层或私有方法上,AOP无法切入。
3.数据源名称写错@DS(“slave”)但配置中数据源名是slave1
1. 检查方法是否被@Transactional注解。如果是,考虑使用@DSTransactional或移除事务注解(对于纯查询)。
2. 确保@DS注解在Service层的public方法上,或者其所属的类上。
3. 仔细核对@DS注解中的字符串与application.yml中配置的数据源名称(如datasource.master里的master)是否完全一致,包括大小写。
@DSTransactional内跨数据源操作,数据源不切换@DSTransactional的设计就是不支持跨数据源事务。它只是确保在它管理的事务内,所有操作使用同一个数据源。重新设计业务逻辑:这是架构限制,不是bug。将需要跨库写操作拆分成多个方法,分别用不同的@DSTransactional管理,并考虑最终一致性方案。或者评估是否真的需要强一致性,如果必须,引入分布式事务框架。
多数据源下,MyBatis二级缓存混乱如果多个SqlSessionFactory共享了同一个缓存实例(默认是同一个),可能导致从不同数据库查到的数据被错误缓存。为每个SqlSessionFactory配置独立的缓存实例。在配置类中创建SqlSessionFactoryBean时,通过setCache方法指定不同的Cache实现或配置不同的cacheNamespace更简单的做法是,在生产环境直接关闭二级缓存,用Redis等外部缓存更可控。
连接池资源耗尽为每个数据源配置的连接池大小总和超过了数据库服务器的最大连接数限制。1.合理规划:根据每个数据源的实际压力(读写比、QPS)分别设置maximum-pool-size。报表库、日志库可以设小点。
2.监控:启用HikariCP的JMX监控或通过/actuator/metrics/hikaricp.connections.*端点查看连接池状态。
3.设置超时:务必配置connection-timeout(获取连接超时)和idle-timeout(连接空闲超时),防止连接泄露。
从库延迟导致读到旧数据(读写分离场景)业务上在写入主库后立刻查询从库,由于主从复制有延迟,可能查不到刚写入的数据。1.强制读主:对于需要强一致性的读请求,使用@DS(“master”)强制走主库。
2.延迟查询:写入后,如果业务允许,等待几百毫秒再查从库。
3.业务设计:区分“实时性要求高”和“允许延迟”的查询,分别路由到主库和从库。

性能优化心得:

  1. 连接池配置不是越大越好maximum-pool-size应该根据数据库服务器性能和业务并发量来定。一个经验公式是:连接数 ≈ (核心数 * 2) + 有效磁盘数。对于Web应用,初始可以设置为CPU核心数的2-4倍,再根据监控调整。设置过大反而会导致数据库负载过高,上下文切换频繁。
  2. 善用minimum-idle:对于流量波动大的应用,可以设置一个较小的minimum-idle(如2-5),让连接池在空闲时收缩,高峰时再扩容。避免长期占用大量空闲连接。
  3. 为不同用途的数据源设置不同的超时时间:对于核心交易库,connection-timeout可以设短一点(如3秒),快速失败。对于报表查询库,可以设长一点(如10秒),因为查询可能本来就很慢。
  4. 监控与告警:一定要将数据源的健康状态(连接数、活跃数、等待数)纳入监控。当连接等待时间(connection-timeout)频繁超时,或者空闲连接(idle-connections)长期为0,都是需要扩容或优化SQL的强烈信号。

配置多数据源,从“配通”到“配优”,是一个需要结合业务特性和监控数据不断调整的过程。希望这篇超详细的指南,能帮你不仅跑通代码,更能理解背后的原理,在遇到问题时能快速找到方向。记住,技术选型没有绝对的好坏,只有适合与否。对于简单固定的多库访问,手动配置清晰可控;对于需要动态路由和读写分离的场景,dynamic-datasource这类组件能极大提升开发效率。根据你的实际场景,做出最适合的选择吧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询