Spring+MyBatis多数据源配置实现:从入门到实践
在复杂的业务场景中,多数据源是常见的技术需求:
- 读写分离:主库(Master)负责写操作,从库(Slave)负责读操作,提升系统吞吐量;
- 分库分表:将数据分散到多个数据库,解决单库性能瓶颈;
- 跨系统集成:同一个服务需要访问多个业务线的数据库(如电商系统同时访问订单库和用户库)。
Spring 提供了灵活的数据源管理机制,而 MyBatis 作为优秀的持久层框架,能与 Spring 无缝整合。本文将详细讲解 Spring+MyBatis 多数据源的两种核心实现方式(动态路由/静态绑定),结合最佳实践和完整示例,帮助你快速落地多数据源方案。
目录#
- 多数据源基础概念
- 前置知识与环境准备
- 实现方式一:动态路由(AbstractRoutingDataSource)
- 3.1 核心原理
- 3.2 step-by-step 实现
- 3.3 数据源切换方式(AOP/手动)
- 实现方式二:静态绑定(多SqlSessionFactory)
- 最佳实践与避坑指南
- 常见场景示例:读写分离
- 高级话题:动态数据源与分布式事务
- 测试策略
- 总结
- 参考资料
1. 多数据源基础概念#
多数据源指一个应用同时连接多个数据库实例(或 schema)。在 Spring+MyBatis 体系中,核心组件的关系如下:
- DataSource:数据库连接池(如 HikariCP),是多数据源的基础;
- SqlSessionFactory:MyBatis 的核心工厂,每个 SqlSessionFactory 绑定一个 DataSource;
- SqlSession:MyBatis 的会话对象,由 SqlSessionFactory 创建;
- Mapper:MyBatis 的映射接口,由 SqlSession 生成代理对象。
多数据源的核心问题是如何将不同的数据库操作路由到对应的 DataSource,常见方案分为两类:
- 动态路由:通过
AbstractRoutingDataSource动态切换数据源(适合频繁切换场景,如读写分离); - 静态绑定:为每个数据源配置独立的
SqlSessionFactory和Mapper(适合固定模块绑定固定数据源,如跨系统集成)。
2. 前置知识与环境准备#
2.1 技术栈要求#
- 基础:Spring Boot、MyBatis、JDBC 核心概念;
- 工具:Maven/Gradle、IntelliJ IDEA、MySQL(或其他数据库);
- 依赖:Spring Boot Starter、MyBatis Starter、连接池(HikariCP)、数据库驱动。
2.2 环境搭建#
2.2.1 依赖配置(pom.xml)#
<dependencies>
<!-- Spring Boot 基础 -->
<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>
<!-- MyBatis 整合 Spring Boot -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- 数据库驱动(MySQL 为例) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 连接池(HikariCP,Spring Boot 2.x 默认) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>2.2.2 配置文件(application.yml)#
假设我们有**主库(Master)和从库(Slave)**两个数据源,配置如下:
spring:
datasource:
# 主库(写操作)
master:
url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 从库(读操作)
slave:
url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/**/*.xml # Mapper XML 路径
type-aliases-package: com.example.demo.entity # 实体类别名包3. 实现方式一:动态路由(AbstractRoutingDataSource)#
3.1 核心原理#
AbstractRoutingDataSource 是 Spring 提供的动态数据源路由抽象类,其核心逻辑是:
- 维护一个目标数据源映射(
targetDataSources),键为数据源标识(如 "master"/"slave"); - 通过
determineCurrentLookupKey()方法动态获取当前数据源标识; - 将数据库操作转发到对应的目标数据源。
3.2 step-by-step 实现#
3.2.1 定义数据源标识(枚举)#
public enum DataSourceType {
MASTER, SLAVE
}3.2.2 线程上下文持有器(ThreadLocal)#
用于存储当前线程的数据源标识(避免线程安全问题):
public class DataSourceContextHolder {
// ThreadLocal 存储当前线程的数据源类型
private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
// 设置数据源类型
public static void setDataSourceType(DataSourceType type) {
CONTEXT_HOLDER.set(type);
}
// 获取数据源类型
public static DataSourceType getDataSourceType() {
return CONTEXT_HOLDER.get();
}
// 清除数据源类型(必须在请求结束时调用,避免内存泄漏)
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}3.2.3 实现动态路由数据源#
继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey() 方法:
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从上下文持有器中获取当前数据源类型
return DataSourceContextHolder.getDataSourceType();
}
}3.2.4 配置数据源与路由#
创建 Spring 配置类,完成以下步骤:
- 配置主库和从库的
DataSourceBean; - 将两个数据源注入
RoutingDataSource的targetDataSources; - 配置
SqlSessionFactory(依赖RoutingDataSource); - 配置事务管理器(
DataSourceTransactionManager)。
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper") // 扫描所有 Mapper 接口
public class MyBatisConfig {
// 绑定主库配置(对应 application.yml 中的 spring.datasource.master)
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
// 绑定从库配置
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
// 配置动态路由数据源
@Bean
public DataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
RoutingDataSource routingDataSource = new RoutingDataSource();
// 目标数据源映射:键为 DataSourceType,值为数据源实例
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER, masterDataSource);
targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
routingDataSource.setTargetDataSources(targetDataSources);
// 默认数据源(当无法获取标识时使用)
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
// 配置 SqlSessionFactory(依赖路由数据源)
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource routingDataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(routingDataSource);
// 配置 MyBatis mapper 文件路径(与 application.yml 一致)
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/**/*.xml"));
return factoryBean.getObject();
}
// 配置事务管理器(依赖路由数据源)
@Bean
public DataSourceTransactionManager transactionManager(DataSource routingDataSource) {
return new DataSourceTransactionManager(routingDataSource);
}
}3.3 数据源切换方式#
3.3.1 手动切换(基础方式)#
在业务方法中显式设置数据源标识:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 写操作:使用主库
@Transactional(rollbackFor = Exception.class)
public void saveUser(User user) {
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
userMapper.insert(user);
// 清除标识(或在全局拦截器中处理)
DataSourceContextHolder.clearDataSourceType();
}
// 读操作:使用从库
public User getUserById(Long id) {
DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
User user = userMapper.selectById(id);
DataSourceContextHolder.clearDataSourceType();
return user;
}
}3.3.2 AOP 切换(推荐方式)#
手动切换需要重复写 set/clear 代码,通过AOP+自定义注解可以实现无侵入式切换:
- 定义自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
DataSourceType value() default DataSourceType.MASTER;
}- 实现 AOP 切面:
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 确保切面优先于事务执行
public class DataSourceAspect {
// 切入点:拦截所有标注 @DataSource 注解的方法
@Pointcut("@annotation(com.example.demo.annotation.DataSource)")
public void dataSourcePointCut() {}
// 环绕通知:设置数据源标识
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取方法上的 @DataSource 注解
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource != null) {
// 设置数据源类型
DataSourceContextHolder.setDataSourceType(dataSource.value());
}
try {
// 执行目标方法
return joinPoint.proceed();
} finally {
// 清除数据源类型(必须执行)
DataSourceContextHolder.clearDataSourceType();
}
}
}- 业务方法中使用注解:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 写操作:使用主库(默认)
@Transactional(rollbackFor = Exception.class)
@DataSource(DataSourceType.MASTER)
public void saveUser(User user) {
userMapper.insert(user);
}
// 读操作:使用从库
@DataSource(DataSourceType.SLAVE)
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}4. 实现方式二:静态绑定(多SqlSessionFactory)#
4.1 适用场景#
当不同模块的 Mapper 固定绑定不同数据源时(如订单模块用订单库,用户模块用用户库),适合使用静态绑定:为每个数据源配置独立的 SqlSessionFactory 和 Mapper。
4.2 step-by-step 实现#
4.2.1 配置多数据源#
假设我们有两个数据源:order_db(订单库)和 user_db(用户库),修改 application.yml:
spring:
datasource:
order:
url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
user:
url: jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=UTC
username: root
password: 1234564.2.2 配置订单库相关 Bean#
@Configuration
// 扫描订单模块的 Mapper(独立包路径)
@MapperScan(basePackages = "com.example.demo.mapper.order", sqlSessionFactoryRef = "orderSqlSessionFactory")
public class OrderDataSourceConfig {
// 订单库 DataSource
@Bean(name = "orderDataSource")
@ConfigurationProperties(prefix = "spring.datasource.order")
public DataSource orderDataSource() {
return DataSourceBuilder.create().build();
}
// 订单库 SqlSessionFactory
@Bean(name = "orderSqlSessionFactory")
public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/order/**/*.xml")); // 订单 Mapper XML 路径
return factoryBean.getObject();
}
// 订单库 SqlSessionTemplate
@Bean(name = "orderSqlSessionTemplate")
public SqlSessionTemplate orderSqlSessionTemplate(@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
// 订单库事务管理器
@Bean(name = "orderTransactionManager")
public DataSourceTransactionManager orderTransactionManager(@Qualifier("orderDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}4.2.3 配置用户库相关 Bean#
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.user", sqlSessionFactoryRef = "userSqlSessionFactory")
public class UserDataSourceConfig {
@Bean(name = "userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user")
public DataSource userDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "userSqlSessionFactory")
public SqlSessionFactory userSqlSessionFactory(@Qualifier("userDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/user/**/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "userSqlSessionTemplate")
public SqlSessionTemplate userSqlSessionTemplate(@Qualifier("userSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = "userTransactionManager")
public DataSourceTransactionManager userTransactionManager(@Qualifier("userDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}4.2.4 业务使用#
@Service
public class OrderService {
// 自动注入订单库 Mapper(由 @MapperScan 扫描)
@Autowired
private OrderMapper orderMapper;
// 自动注入用户库 Mapper
@Autowired
private UserMapper userMapper;
// 订单库事务:使用 orderTransactionManager
@Transactional(transactionManager = "orderTransactionManager", rollbackFor = Exception.class)
public void createOrder(Order order) {
orderMapper.insert(order);
}
// 用户库查询:使用 userSqlSessionTemplate
public User getUserById(Long userId) {
return userMapper.selectById(userId);
}
}5. 最佳实践与避坑指南#
5.1 必遵循的最佳实践#
- 使用连接池:优先选择 HikariCP(Spring Boot 2.x 默认,性能最优),配置合理的池大小(如
minimum-idle: 5、maximum-pool-size: 20); - 清除 ThreadLocal:使用
DataSourceContextHolder时,必须在请求结束或方法返回时清除(否则会导致内存泄漏); - 事务与数据源一致性:事务方法中不要切换数据源(否则事务会失效,因为事务绑定的是初始数据源);
- AOP 优先级:确保数据源切换的 AOP 切面优先级高于事务切面(
@Order(Ordered.HIGHEST_PRECEDENCE)); - 监控与报警:使用 Spring Boot Actuator 监控数据源连接池状态(如
spring.boot.actuator.endpoints.web.exposure.include=health,metrics)。
5.2 常见坑点与解决#
| 问题 | 原因 | 解决 |
|---|---|---|
| 线程安全问题 | 未使用 ThreadLocal 存储数据源标识 | 必须用 ThreadLocal 持有数据源标识 |
| 内存泄漏 | 未清除 ThreadLocal 中的值 | 在 finally 块或全局拦截器中调用 clear() |
| 事务失效 | 事务方法中切换数据源 | 事务开始前切换数据源,或使用分布式事务 |
| 数据源未找到 | targetDataSources 未配置对应键 | 检查 DataSourceType 与 targetDataSources 的键是否一致 |
6. 常见场景示例:读写分离#
6.1 场景描述#
- 主库(Master):处理写操作(INSERT/UPDATE/DELETE);
- 从库(Slave):处理读操作(SELECT);
- 要求:写操作自动路由到主库,读操作自动路由到从库。
6.2 实现步骤#
6.3 验证#
- 写操作:调用
saveUser(),检查主库是否插入数据; - 读操作:调用
getUserById(),检查从库是否查询到数据(需确保主从同步正常)。
7. 高级话题#
7.1 动态数据源注册( runtime 新增)#
如果需要在运行时动态添加数据源(如 SaaS 多租户场景),可以通过 Spring 的 BeanDefinitionRegistry 实现:
@Service
public class DynamicDataSourceService {
@Autowired
private ApplicationContext applicationContext;
// 动态注册数据源
public void registerDataSource(String dataSourceName, DataSourceProperties properties) {
// 创建 DataSource BeanDefinition
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(HikariDataSource.class);
builder.addPropertyValue("jdbcUrl", properties.getUrl());
builder.addPropertyValue("username", properties.getUsername());
builder.addPropertyValue("password", properties.getPassword());
builder.addPropertyValue("driverClassName", properties.getDriverClassName());
// 注册 Bean
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory();
registry.registerBeanDefinition(dataSourceName + "DataSource", builder.getBeanDefinition());
// 更新 RoutingDataSource 的 targetDataSources
RoutingDataSource routingDataSource = (RoutingDataSource) applicationContext.getBean("routingDataSource");
Map<Object, Object> targetDataSources = new HashMap<>(routingDataSource.getTargetDataSources());
targetDataSources.put(dataSourceName, applicationContext.getBean(dataSourceName + "DataSource"));
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.afterPropertiesSet(); // 重新初始化
}
}7.2 MyBatis Plus 多数据源(简化方案)#
MyBatis Plus 提供了动态数据源 starter(mybatis-plus-boot-starter-dynamic-datasource),可以零配置实现多数据源:
- 添加依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>- 配置多数据源:
spring:
datasource:
dynamic:
primary: master # 默认数据源
datasource:
master:
url: jdbc:mysql://localhost:3306/master_db
username: root
password: 123456
slave:
url: jdbc:mysql://localhost:3306/slave_db
username: root
password: 123456- 业务使用(
@DS注解):
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@DS("master") // 写操作:主库
@Transactional
public void saveUser(User user) {
userMapper.insert(user);
}
@DS("slave") // 读操作:从库
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}7.3 分布式事务处理#
当业务需要跨多个数据源执行事务(如同时修改订单库和用户库),单数据源事务管理器(DataSourceTransactionManager)无法满足需求,需使用分布式事务框架:
- Seata:阿里开源的高性能分布式事务解决方案,支持 AT(自动补偿)、TCC 模式;
- Atomikos:开源的 JTA 事务管理器,支持 XA 协议。
8. 测试策略#
8.1 单元测试(验证数据源切换)#
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private DataSource routingDataSource;
@Test
void testSaveUser() {
// 1. 调用写方法(主库)
User user = new User();
user.setName("test");
userService.saveUser(user);
// 2. 验证数据源是否为 MASTER
DataSourceType currentType = DataSourceContextHolder.getDataSourceType();
Assertions.assertEquals(DataSourceType.MASTER, currentType);
}
@Test
void testGetUserById() {
// 1. 调用读方法(从库)
User user = userService.getUserById(1L);
// 2. 验证数据源是否为 SLAVE
DataSourceType currentType = DataSourceContextHolder.getDataSourceType();
Assertions.assertEquals(DataSourceType.SLAVE, currentType);
}
}8.2 集成测试(验证数据一致性)#
- 写操作:调用
saveUser()后,检查主库是否存在数据; - 读操作:调用
getUserById()后,检查从库是否同步到数据(需主从复制正常)。
9. 总结#
- 动态路由(AbstractRoutingDataSource):适合频繁切换数据源(如读写分离),通过 ThreadLocal+AOP 实现无侵入切换;
- 静态绑定(多 SqlSessionFactory):适合固定模块绑定固定数据源(如跨系统集成),配置独立的 Mapper 和事务管理器;
- 最佳实践:优先使用连接池、清除 ThreadLocal、确保事务与数据源一致;
- 高级扩展:MyBatis Plus 简化多数据源配置,Seata 解决分布式事务问题。
10. 参考资料#
- Spring 官方文档:AbstractRoutingDataSource;
- MyBatis 官方文档:Spring Integration;
- MyBatis Plus 动态数据源:Dynamic Datasource;
- Seata 官方文档:Seata 快速开始;
- 《Spring Boot 实战》:第 6 章 数据访问。