SpringBoot 整合多数据源:从基础切换到动态路由全解析
在实际开发中,单数据源往往无法满足复杂的业务场景 —— 比如读写分离、分库分表、不同业务模块对接不同数据库等。SpringBoot 作为主流的开发框架,提供了多种多数据源整合方案,从简单的静态切换到灵活的动态路由,每种方案都有其适用场景。本文将从实际业务需求出发,拆解 SpringBoot 中多数据源的核心实现方式,并附上可直接运行的代码示例。
一、多数据源核心场景与技术选型
先明确多数据源的常见使用场景,避免盲目选型:
- 静态多数据源:不同业务模块固定对接不同数据库(如订单库、用户库),启动时加载,运行时不切换;
- 动态切换数据源:运行时根据条件(如用户 ID、业务标识)动态选择数据源(如读写分离、分库);
- 分布式事务:多数据源操作需保证事务一致性(本文暂不展开,后续单独讲解)。
核心依赖(基于 SpringBoot 2.7.x):
xml
<dependencies> <!-- SpringBoot核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据库连接 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- 数据库驱动(以MySQL为例) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.16</version> </dependency> <!-- 测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> 二、方案 1:静态多数据源(基于配置类分离)
适用场景
不同业务模块完全隔离,比如「用户模块」对接 user_db,「订单模块」对接 order_db,运行时无需切换数据源。
实现步骤
1. 配置文件(application.yml)
yaml
spring: datasource: # 数据源1:用户库 user: url: jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # 数据源2:订单库 order: url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # MyBatis-Plus配置 mybatis-plus: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true 2. 数据源配置类
分别配置两个数据源的 Bean,指定不同的扫描路径:
用户数据源配置(UserDataSourceConfig.java):
java
运行
package com.example.demo.config; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.type.JdbcType; import org.mybatis.spring.SqlSessionTemplate; 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 javax.sql.DataSource; /** * 用户库数据源配置 * 扫描user模块的mapper */ @Configuration @MapperScan(basePackages = "com.example.demo.mapper.user", sqlSessionTemplateRef = "userSqlSessionTemplate") public class UserDataSourceConfig { /** * 配置用户库数据源 */ @Bean(name = "userDataSource") @ConfigurationProperties(prefix = "spring.datasource.user") @Primary // 主数据源(必须指定一个主数据源) public DataSource userDataSource() { return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build(); } /** * 配置用户库SqlSessionFactory */ @Bean(name = "userSqlSessionFactory") @Primary public SqlSessionFactory userSqlSessionFactory(@Qualifier("userDataSource") DataSource dataSource, MybatisPlusInterceptor interceptor) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); // 配置MyBatis-Plus MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class); configuration.setJdbcTypeForNull(JdbcType.NULL); sqlSessionFactory.setConfiguration(configuration); // 分页插件(可选) sqlSessionFactory.setPlugins(interceptor); // Mapper文件路径 sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/user/*.xml")); return sqlSessionFactory.getObject(); } /** * 配置用户库SqlSessionTemplate */ @Bean(name = "userSqlSessionTemplate") @Primary public SqlSessionTemplate userSqlSessionTemplate(@Qualifier("userSqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } } 订单数据源配置(OrderDataSourceConfig.java):
java
运行
package com.example.demo.config; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.type.JdbcType; import org.mybatis.spring.SqlSessionTemplate; 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 javax.sql.DataSource; /** * 订单库数据源配置 * 扫描order模块的mapper */ @Configuration @MapperScan(basePackages = "com.example.demo.mapper.order", sqlSessionTemplateRef = "orderSqlSessionTemplate") public class OrderDataSourceConfig { @Bean(name = "orderDataSource") @ConfigurationProperties(prefix = "spring.datasource.order") public DataSource orderDataSource() { return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build(); } @Bean(name = "orderSqlSessionFactory") public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource, MybatisPlusInterceptor interceptor) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class); configuration.setJdbcTypeForNull(JdbcType.NULL); sqlSessionFactory.setConfiguration(configuration); sqlSessionFactory.setPlugins(interceptor); sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/order/*.xml")); return sqlSessionFactory.getObject(); } @Bean(name = "orderSqlSessionTemplate") public SqlSessionTemplate orderSqlSessionTemplate(@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } } 3. 业务代码示例
- 用户 Mapper(UserMapper.java):放在
com.example.demo.mapper.user包下
java
运行
package com.example.demo.mapper.user; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper<User> { } - 订单 Mapper(OrderMapper.java):放在
com.example.demo.mapper.order包下
java
运行
package com.example.demo.mapper.order; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.Order; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OrderMapper extends BaseMapper<Order> { } - 业务层调用:
java
运行
package com.example.demo.service.impl; import com.example.demo.entity.Order; import com.example.demo.entity.User; import com.example.demo.mapper.order.OrderMapper; import com.example.demo.mapper.user.UserMapper; import com.example.demo.service.DataSourceService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class DataSourceServiceImpl implements DataSourceService { @Resource private UserMapper userMapper; @Resource private OrderMapper orderMapper; @Override public User getUserById(Long id) { // 自动使用用户库数据源 return userMapper.selectById(id); } @Override public Order getOrderById(Long id) { // 自动使用订单库数据源 return orderMapper.selectById(id); } } 方案 1 优缺点
✅ 优点:配置简单、无侵入性、性能高,适合模块隔离的场景;❌ 缺点:无法动态切换,新增数据源需新增配置类,灵活性低。
三、方案 2:动态切换数据源(基于 AbstractRoutingDataSource)
适用场景
需要根据业务逻辑动态切换数据源,比如「读写分离」(读库 / 写库切换)、「分库」(按用户 ID 路由到不同库)。
核心思路:继承 Spring 提供的AbstractRoutingDataSource,重写determineCurrentLookupKey方法,通过 ThreadLocal 存储当前线程的数据源标识,实现动态路由。
实现步骤
1. 配置文件(application.yml)
yaml
spring: datasource: # 主库(写) master: url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 从库(读) slave: url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource 2. 核心工具类
数据源上下文 Holder(DynamicDataSourceContextHolder.java):
java
运行
package com.example.demo.config.dynamic; /** * 数据源上下文 Holder,基于ThreadLocal存储当前线程的数据源标识 */ public class DynamicDataSourceContextHolder { /** * 线程本地变量:存储当前线程使用的数据源标识 */ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 设置数据源标识 */ public static void setDataSourceKey(String key) { CONTEXT_HOLDER.set(key); } /** * 获取数据源标识 */ public static String getDataSourceKey() { return CONTEXT_HOLDER.get(); } /** * 清除数据源标识 */ public static void clearDataSourceKey() { CONTEXT_HOLDER.remove(); } } 动态数据源路由类(DynamicRoutingDataSource.java):
java
运行
package com.example.demo.config.dynamic; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 动态数据源路由:重写determineCurrentLookupKey方法,返回当前线程的数据源标识 */ public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 从ThreadLocal中获取当前线程的数据源标识 return DynamicDataSourceContextHolder.getDataSourceKey(); } } 3. 数据源配置类
java
运行
package com.example.demo.config.dynamic; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import org.apache.ibatis.session.SqlSessionFactory; 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 javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * 动态数据源配置 */ @Configuration @MapperScan(basePackages = "com.example.demo.mapper", sqlSessionFactoryRef = "dynamicSqlSessionFactory") public class DynamicDataSourceConfig { /** * 配置主库数据源 */ @Bean(name = "masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build(); } /** * 配置从库数据源 */ @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build(); } /** * 配置动态数据源(核心) */ @Bean(name = "dynamicDataSource") @Primary public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) { DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(); // 1. 配置默认数据源(主库) dynamicRoutingDataSource.setDefaultTargetDataSource(masterDataSource); // 2. 配置所有数据源(key为数据源标识,value为数据源) Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put("master", masterDataSource); dataSourceMap.put("slave", slaveDataSource); dynamicRoutingDataSource.setTargetDataSources(dataSourceMap); return dynamicRoutingDataSource; } /** * 配置SqlSessionFactory */ @Bean(name = "dynamicSqlSessionFactory") public SqlSessionFactory dynamicSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource, MybatisPlusInterceptor interceptor) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/**/*.xml")); sqlSessionFactory.setPlugins(interceptor); return sqlSessionFactory.getObject(); } } 4. 自定义注解 + AOP 实现自动切换
数据源注解(DataSource.java):
java
运行
package com.example.demo.annotation; import java.lang.annotation.*; /** * 自定义数据源注解:标注在方法/类上,指定使用的数据源 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { /** * 数据源标识,对应dynamicDataSource中的key */ String value() default "master"; } AOP 切面(DataSourceAspect.java):
java
运行
package com.example.demo.aspect; import com.example.demo.annotation.DataSource; import com.example.demo.config.dynamic.DynamicDataSourceContextHolder; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * 数据源切换切面:拦截@DataSource注解,设置对应的数据源标识 */ @Aspect @Component @Order(-1) // 保证切面优先级高于事务 public class DataSourceAspect { /** * 切入点:拦截所有标注@DataSource的方法/类 */ @Pointcut("@annotation(com.example.demo.annotation.DataSource) || @within(com.example.demo.annotation.DataSource)") public void dataSourcePointCut() {} /** * 环绕通知:切换数据源 */ @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { // 1. 获取注解中的数据源标识 String dataSourceKey = getDataSourceKey(point); // 2. 设置数据源标识到ThreadLocal DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey); try { // 3. 执行目标方法 return point.proceed(); } finally { // 4. 清除数据源标识,避免线程复用导致的问题 DynamicDataSourceContextHolder.clearDataSourceKey(); } } /** * 获取方法/类上的数据源标识 */ private String getDataSourceKey(ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); // 优先获取方法上的注解 DataSource methodAnnotation = method.getAnnotation(DataSource.class); if (methodAnnotation != null) { return methodAnnotation.value(); } // 方法上没有则获取类上的注解 Class<?> targetClass = point.getTarget().getClass(); DataSource classAnnotation = targetClass.getAnnotation(DataSource.class); if (classAnnotation != null) { return classAnnotation.value(); } // 默认使用主库 return "master"; } } 5. 业务代码示例
java
运行
package com.example.demo.service.impl; import com.example.demo.annotation.DataSource; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.example.demo.service.UserService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; /** * 写操作:使用主库 */ @Override @DataSource("master") public void saveUser(User user) { userMapper.insert(user); } /** * 读操作:使用从库 */ @Override @DataSource("slave") public User getUserById(Long id) { return userMapper.selectById(id); } } 方案 2 优缺点
✅ 优点:灵活性高,支持运行时动态切换,可扩展至 N 个数据源;❌ 缺点:需要手动管理 ThreadLocal,切面优先级需高于事务,否则切换失效。
四、方案 3:基于 MyBatis-Plus 插件(dynamic-datasource-spring-boot-starter)
适用场景
追求极简配置,快速实现多数据源切换(推荐生产环境使用)。
dynamic-datasource-spring-boot-starter 是 MyBatis-Plus 团队提供的多数据源插件,封装了 AbstractRoutingDataSource 的底层逻辑,支持注解、spel 表达式、分布式场景等,无需手动编写切面和路由类。
实现步骤
1. 引入依赖
xml
<!-- 动态数据源插件(核心) --> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.6.1</version> </dependency> 2. 配置文件(application.yml)
yaml
spring: # 动态数据源配置 dynamic: datasource: # 主库(默认) master: url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 从库1 slave1: url: jdbc:mysql://localhost:3306/slave1_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 从库2 slave2: url: jdbc:mysql://localhost:3306/slave2_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 配置默认数据源 primary: master # 配置Druid连接池 type: com.alibaba.druid.pool.DruidDataSource 3. 业务代码示例
直接使用插件提供的@DS注解切换数据源:
java
运行
package com.example.demo.service.impl; import com.baomidou.dynamic.datasource.annotation.DS; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.example.demo.service.UserService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; /** * 默认使用主库(可不加注解) */ @Override public void saveUser(User user) { userMapper.insert(user); } /** * 使用从库1 */ @Override @DS("slave1") public User getUserById(Long id) { return userMapper.selectById(id); } /** * 使用从库2 */ @Override @DS("slave2") public User getUserByPhone(String phone) { return userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone)); } } 进阶用法:spel 表达式动态路由
支持根据方法参数动态选择数据源(比如按用户 ID 取模分库):
java
运行
/** * 根据用户ID取模,路由到不同从库 */ @Override @DS("#{id % 2 == 0 ? 'slave1' : 'slave2'}") public User getUserById(Long id) { return userMapper.selectById(id); } 方案 3 优缺点
✅ 优点:零配置成本、功能丰富(支持读写分离、负载均衡、分布式锁)、官方维护;❌ 缺点:依赖第三方插件,深度定制化场景需二次开发。
五、方案对比与选型建议
表格
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 静态多数据源(配置类) | 配置简单、性能高、无侵入 | 无法动态切换、扩展性差 | 模块隔离的固定多数据源场景 |
| 自定义动态数据源 | 高度自定义、灵活性高 | 需手动编写代码、易出问题 | 特殊定制化的动态切换场景 |
| dynamic-datasource | 极简配置、功能丰富、稳定性高 | 依赖第三方插件 | 大部分生产环境(推荐) |
六、注意事项
- 事务问题:动态数据源切换需保证切面优先级高于事务(@Order (-1)),否则事务内切换数据源失效;
- 线程安全:ThreadLocal 需在方法执行完毕后清除,避免线程池复用导致数据源串用;
- 连接池配置:多数据源场景下需合理配置连接池大小,避免连接耗尽;
- 读写分离:从库建议设置为只读,避免写入数据导致主从同步异常。
总结
SpringBoot 整合多数据源的核心思路是「数据源路由」,不同方案只是封装程度不同。对于大部分开发者来说,优先选择 dynamic-datasource 插件,兼顾效率和稳定性;特殊定制化场景可基于 AbstractRoutingDataSource 手动实现;模块隔离场景则用静态多数据源即可。
本文所有代码均可直接复制运行,建议根据实际业务场景调整数据源配置和切换逻辑。如果有分布式事务、分库分表等进阶需求,可关注后续文章。