SpringBoot实现整合多数据源的全攻略
作者:我可能在学习如何写bug
在实际开发中,单数据源往往无法满足复杂的业务场景 —— 比如读写分离、分库分表、不同业务模块对接不同数据库等。SpringBoot 作为主流的开发框架,提供了多种多数据源整合方案,从简单的静态切换到灵活的动态路由,每种方案都有其适用场景。本文将从实际业务需求出发,拆解 SpringBoot 中多数据源的核心实现方式,并附上可直接运行的代码示例。
一、多数据源核心场景与技术选型
先明确多数据源的常见使用场景,避免盲目选型:
- 静态多数据源:不同业务模块固定对接不同数据库(如订单库、用户库),启动时加载,运行时不切换;
- 动态切换数据源:运行时根据条件(如用户 ID、业务标识)动态选择数据源(如读写分离、分库);
- 分布式事务:多数据源操作需保证事务一致性(本文暂不展开,后续单独讲解)。
核心依赖(基于 SpringBoot 2.7.x):
<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,运行时无需切换数据源。
实现步骤
配置文件(application.yml)
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数据源配置类
分别配置两个数据源的 Bean,指定不同的扫描路径:
用户数据源配置(UserDataSourceConfig.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):
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);
}
}
业务代码示例
用户 Mapper(UserMapper.java):放在com.example.demo.mapper.user包下
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包下
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> {
}
业务层调用:
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);
}
}
优缺点
优点:配置简单、无侵入性、性能高,适合模块隔离的场景;
缺点:无法动态切换,新增数据源需新增配置类,灵活性低。
三、方案 2:动态切换数据源(基于 AbstractRoutingDataSource)
适用场景
需要根据业务逻辑动态切换数据源,比如「读写分离」(读库 / 写库切换)、「分库」(按用户 ID 路由到不同库)。
核心思路:继承 Spring 提供的AbstractRoutingDataSource,重写determineCurrentLookupKey方法,通过 ThreadLocal 存储当前线程的数据源标识,实现动态路由。
实现步骤
配置文件(application.yml)
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核心工具类
数据源上下文 Holder(DynamicDataSourceContextHolder.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):
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();
}
}
数据源配置类
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();
}
}
自定义注解 + AOP 实现自动切换
数据源注解(DataSource.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):
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";
}
}
业务代码示例
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);
}
}
优缺点
优点:灵活性高,支持运行时动态切换,可扩展至 N 个数据源;
缺点:需要手动管理 ThreadLocal,切面优先级需高于事务,否则切换失效。
四、方案 3:基于 MyBatis-Plus 插件(dynamic-datasource-spring-boot-starter)
适用场景
追求极简配置,快速实现多数据源切换(推荐生产环境使用)。
dynamic-datasource-spring-boot-starter 是 MyBatis-Plus 团队提供的多数据源插件,封装了 AbstractRoutingDataSource 的底层逻辑,支持注解、spel 表达式、分布式场景等,无需手动编写切面和路由类。
实现步骤
引入依赖
<!-- 动态数据源插件(核心) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>配置文件(application.yml)
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业务代码示例
直接使用插件提供的@DS注解切换数据源:
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 取模分库):
/**
* 根据用户ID取模,路由到不同从库
*/
@Override
@DS("#{id % 2 == 0 ? 'slave1' : 'slave2'}")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
优缺点
优点:零配置成本、功能丰富(支持读写分离、负载均衡、分布式锁)、官方维护;
缺点:依赖第三方插件,深度定制化场景需二次开发。
五、方案对比与选型建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 静态多数据源(配置类) | 配置简单、性能高、无侵入 | 无法动态切换、扩展性差 | 模块隔离的固定多数据源场景 |
| 自定义动态数据源 | 高度自定义、灵活性高 | 需手动编写代码、易出问题 | 特殊定制化的动态切换场景 |
| dynamic-datasource | 极简配置、功能丰富、稳定性高 | 依赖第三方插件 | 大部分生产环境(推荐) |
六、注意事项
- 事务问题:动态数据源切换需保证切面优先级高于事务(@Order (-1)),否则事务内切换数据源失效;
- 线程安全:ThreadLocal 需在方法执行完毕后清除,避免线程池复用导致数据源串用;
- 连接池配置:多数据源场景下需合理配置连接池大小,避免连接耗尽;
- 读写分离:从库建议设置为只读,避免写入数据导致主从同步异常。
总结
SpringBoot 整合多数据源的核心思路是「数据源路由」,不同方案只是封装程度不同。对于大部分开发者来说,优先选择 dynamic-datasource 插件,兼顾效率和稳定性;特殊定制化场景可基于 AbstractRoutingDataSource 手动实现;模块隔离场景则用静态多数据源即可。
本文所有代码均可直接复制运行,建议根据实际业务场景调整数据源配置和切换逻辑。
以上就是SpringBoot实现整合多数据源的全攻略的详细内容,更多关于SpringBoot整合多数据源的资料请关注脚本之家其它相关文章!
