在SpringBoot工程里只维护一个数据源通常能满足早期需求,但随着读写分离、业务库拆分或多租户隔离的落地,单个连接池越来越难以支撑。本文不会停留在概念层面,而是直接给出一个能够运行的多数据源改造方案,重点说明Druid连接池如何同时管理多个数据库,Mybatis如何为每个数据源建立独立的SqlSessionFactory,以及怎样通过动态路由让DAO层无感切换。

一、多数据源要解决的核心场景
首先需要明确,多数据源并不是简单地把多个数据库地址写进配置文件。真正要处理的是连接池隔离、Mapper绑定、事务边界和切换时机。常见的需求包括主从复制下的读写分离、订单库与报表库互相独立、不同租户使用不同schema等。如果只注册多个DataSource而不做路由,Mybatis默认只会使用标注了@Primary的那个数据源,其余数据源不会参与SQL执行。
例如同一个服务里,主库负责写入,从库负责查询,此时DAO层不能固定指向某一个数据源,否则读请求会全部压在主库上。又比如业务库和日志库使用同一套Mapper接口,但表结构相同、数据不同,就需要根据调用方传入的标识来动态选择。理解了这些场景,再去看Druid多数据源的配置会更有针对性。
二、Druid与Mybatis的基础整合
依赖层面只需引入druid-spring-boot-starter、mybatis-spring-boot-starter以及数据库驱动。SpringBoot的自动装配会根据spring.datasource前缀创建单个数据源,但多数据源场景下不能依赖自动装配,需要手动注册所有DataSource、SqlSessionFactory和SqlSessionTemplate。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.23</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
这里展示的是Maven依赖片段,实际使用时版本号根据自己的SpringBoot版本调整。Druid的starter会自动注册一个DruidDataSource,但那是基于单数据源默认前缀的,多数据源时我们要手动创建。mybatis-spring-boot-starter则提供了SqlSessionFactoryBean和SqlSessionTemplate,方便自定义。
接下来在application.yml中为每个数据源定义连接信息。注意不要使用spring.datasource作为前缀,避免自动装配冲突。可以自定义spring.datasource.master和spring.datasource.slave之类的结构,也可以完全自定义前缀。
spring:
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
这段配置里master和slave是两个独立的数据源,Druid参数写在各自节点下。实际环境可以配置更多数据源,只需要继续追加。连接池参数不要照搬,建议根据数据库连接数和机器性能调整max-active与min-idle。url中的&是YAML转义后的写法,看起来会比普通URL多一个amp;,这是为了确保解析正确。
配置好连接信息后,需要创建配置类来手动注册数据源。SpringBoot不会自动把这些自定义前缀转换成DataSource,需要借助@ConfigurationProperties或手动读取配置。这里给出一个基于DruidDataSource手动创建的示例。
@Configuration
public class DataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return new DruidDataSource();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return new DruidDataSource();
}
@Bean(name = "masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(
@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/master/*.xml"));
return bean.getObject();
}
@Bean(name = "slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(
@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/slave/*.xml"));
return bean.getObject();
}
@Bean(name = "masterSqlSessionTemplate")
public SqlSessionTemplate masterSqlSessionTemplate(
@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = "slaveSqlSessionTemplate")
public SqlSessionTemplate slaveSqlSessionTemplate(
@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
上面代码创建了两个完全独立的SqlSessionFactory,并分别指定了不同的Mapper XML路径。这样主库和从库的SQL可以分开管理。如果两个数据源的表结构完全一致,也可以共用同一组Mapper XML,但在多租户或分库场景下建议分开,便于后续排查问题。
三、使用AbstractRoutingDataSource实现动态切换
如果每次访问数据库都要手动选择SqlSessionTemplate,代码侵入性会很大。更优雅的做法是让DAO层只调用一个Mapper接口,由底层根据上下文自动决定走哪个数据源。Spring提供了AbstractRoutingDataSource抽象类,它会在获取连接时调用determineCurrentLookupKey()方法,返回值作为数据源的key去匹配之前注册的targetDataSources。
先自定义一个DynamicDataSource继承该类,并维护一个ThreadLocal上下文,保证每个线程的数据源选择互不干扰。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT.set(key);
}
public static String getDataSourceKey() {
return CONTEXT.get();
}
public static void clearDataSourceKey() {
CONTEXT.remove();
}
}
然后将所有真实数据源打包进DynamicDataSource,并把默认数据源设为主库。这样在没有显式切换时,所有请求都会落到主库,避免读请求意外打到从库造成数据不一致。
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", master);
targetDataSources.put("slave", slave);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(master);
return dynamicDataSource;
}
有了动态数据源还不够,需要一个触发切换的机制。最简单的做法是自定义一个注解@DataSource,配合Spring AOP在Service方法执行前设置数据源key,执行后清理。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "master";
}
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void before(DataSource dataSource) {
DataSourceContextHolder.setDataSourceKey(dataSource.value());
}
@After("@annotation(dataSource)")
public void after(DataSource dataSource) {
DataSourceContextHolder.clearDataSourceKey();
}
}
使用时只需要在Service方法上标注@DataSource("slave"),该方法内调用Mapper就会走从库。需要特别注意的是,切换数据源的AOP切面必须作用在Service层,而不是Mapper层。因为Mapper接口的代理对象在Spring容器中生成较早,切面顺序和事务切面的先后会影响最终连接选择。如果切在Controller层,进入Service前切换也有效,但通常不建议放在过外层,避免其他无关逻辑误切换。
四、Druid监控与事务边界问题
Druid提供了强大的监控统计功能,多数据源场景下同样可以开启。需要注册StatViewServlet和WebStatFilter,并在数据源上设置filters为stat,wall。YAML里可以添加druid.filters,但要注意多数据源时每个数据源都要单独配置。
@Bean
public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
ServletRegistrationBean<StatViewServlet> registrationBean =
new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
registrationBean.addInitParameter("loginUsername", "admin");
registrationBean.addInitParameter("loginPassword", "admin123");
return registrationBean;
}
@Bean
public FilterRegistrationBean<WebStatFilter> druidWebStatFilter() {
FilterRegistrationBean<WebStatFilter> filterBean =
new FilterRegistrationBean<>(new WebStatFilter());
filterBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
filterBean.setUrlPatterns(Arrays.asList("/*"));
return filterBean;
}
访问http://localhost:8080/druid/可以查看SQL执行时间、连接池状态和慢查询记录。多数据源时,监控页面会显示所有已注册的数据源实例,点击对应名称即可查看各自连接池的活跃连接数、等待线程数等指标。如果发现某个从库长期空闲或主库连接一直占满,说明路由规则需要调整。
最后是事务问题。多数据源切换最大的坑在于事务管理器。默认的DataSourceTransactionManager绑定的是固定数据源,如果事务开启后动态数据源切换了key,连接可能已经被固定,导致切换失效。单库事务可以继续使用@Transactional,但对于跨库操作,需要引入分布式事务方案,比如Seata或者基于消息的最终一致性,否则不要轻易在一个事务里同时操作主库和从库。
此外,AbstractRoutingDataSource切换的时机是在获取连接时生效。如果事务在Service方法上开启,而数据源切面在事务切面之后执行,那么事务管理器已经拿到连接,后面的切换不会改变当前事务使用的连接。因此注解切换的切面顺序建议设置在事务切面之前,或者在事务方法外完成切换。
五、完整结构回顾与调优建议
按照上面的步骤改造后,工程结构大致包含配置类、动态数据源类、上下文持有类、注解类和切面类。启动项目后,可以用一个只读接口标注@DataSource("slave"),验证返回的数据是否来自从库;再调用写入接口,确认主库产生变更。如果从库连接失败,检查targetDataSources是否注册完整,以及YAML里的URL是否使用了正确的转义。
性能调优方面,Druid的max-active不是越大越好,过多连接会消耗数据库端资源。建议先通过压测确定单库连接数,再决定两个库各自的上限。SQL映射文件尽量按数据源拆分成不同目录,可以减少SqlSessionFactoryBean扫描范围,启动速度也会更快。多数据源环境下日志里会打印多个连接池的初始化信息,如果看到大量DruidDataSource报错,优先检查驱动版本是否匹配。
通过这套方案,新手也能快速实现一个灵活的SpringBoot多数据源工程,后续无论是增加第三个数据源还是接入主从切换,都能在现有架构上平滑扩展。
SpringBoot多数据源Druid连接池Mybatis整合修改时间:2026-09-23 03:47:18