在 Spring Boot 中配置多数据源是一个非常常见的需求,主要用于以下场景:
下面我将详细介绍两种主流的实现方式:
这种方式的核心思想是为每个数据源创建一套独立的配置(DataSource
, SqlSessionFactory
, TransactionManager
),并使用 @MapperScan
注解扫描不同包路径下的 Mapper 接口,将它们绑定到对应的数据源上。
确保有以下依赖。通常 Spring Boot Starter 会包含大部分。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.3.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
dependency>
dependencies>
为不同的数据源定义各自的连接信息,并用不同的前缀区分。
spring:
datasource:
# 主数据源配置 (master)
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_master?serverTimezone=UTC
username: root
password: your_password
type: com.zaxxer.hikari.HikariDataSource # 指定连接池类型
# 从数据源配置 (slave)
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3307/db_slave?serverTimezone=UTC
username: root
password: your_password
type: com.zaxxer.hikari.HikariDataSource
为每个数据源创建一个 Java 配置类。
主数据源配置 (MasterDataSourceConfig.java)
package com.example.config.datasource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
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.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
@Configuration
// 扫描 Master 库的 Mapper 接口
@MapperScan(basePackages = "com.example.mapper.master", sqlSessionTemplateRef = "masterSqlSessionTemplate")
public class MasterDataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
@Primary // 标记为主数据源
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "masterSqlSessionFactory")
@Primary
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 如果有 XML 文件,指定位置
// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/master/*.xml"));
return bean.getObject();
}
@Bean(name = "masterTransactionManager")
@Primary
public DataSourceTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "masterSqlSessionTemplate")
@Primary
public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
从数据源配置 (SlaveDataSourceConfig.java)
package com.example.config.datasource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
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.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
@Configuration
// 扫描 Slave 库的 Mapper 接口
@MapperScan(basePackages = "com.example.mapper.slave", sqlSessionTemplateRef = "slaveSqlSessionTemplate")
public class SlaveDataSourceConfig {
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/slave/*.xml"));
return bean.getObject();
}
@Bean(name = "slaveTransactionManager")
public DataSourceTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "slaveSqlSessionTemplate")
public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
将不同数据源的 Mapper 接口放到各自的包下。
com.example.mapper.master
-> UserMasterMapper.java
com.example.mapper.slave
-> OrderSlaveMapper.java
现在你可以在 Service 中直接注入并使用对应的 Mapper,Spring 会自动为它们关联正确的数据源。
@Service
public class MyService {
@Autowired
private UserMasterMapper userMasterMapper; // 操作 master 库
@Autowired
private OrderSlaveMapper orderSlaveMapper; // 操作 slave 库
public void doSomething() {
// ...
userMasterMapper.insert(someUser); // 写入主库
Order order = orderSlaveMapper.selectById(1); // 从从库读取
}
}
优点:配置隔离,结构非常清晰,不会混淆。
缺点:如果一个 Service 方法需要同时操作两个库,代码会稍微复杂,且默认的 @Transactional
不能跨数据源生效。
这种方式更灵活,适用于读写分离等需要在同一个 Service 中切换数据源的场景。
与方案一相同。
创建一个注解,用于标记方法应该使用哪个数据源。
package com.example.config.dynamic;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master"; // 默认使用 master 数据源
}
使用 ThreadLocal
来存储当前线程需要使用的数据源 Key。
package com.example.config.dynamic;
public class DataSourceContextHolder {
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();
}
}
继承 AbstractRoutingDataSource
,重写 determineCurrentLookupKey
方法,从 DataSourceContextHolder
获取当前数据源 Key。
package com.example.config.dynamic;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
创建一个切面,拦截 @DataSource
注解,在方法执行前设置数据源 Key,在方法执行后清除它。
package com.example.config.dynamic;
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;
@Aspect
@Component
@Order(1) // 保证该AOP在@Transactional之前执行
public class DataSourceAspect {
@Pointcut("@annotation(com.example.config.dynamic.DataSource)")
public void dsPointCut() {}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
// 设置数据源
if (dataSource != null) {
DataSourceContextHolder.setDataSourceKey(dataSource.value());
}
try {
return point.proceed();
} finally {
// 清除数据源,防止内存泄漏
DataSourceContextHolder.clearDataSourceKey();
}
}
}
创建一个统一的配置类来管理所有数据源。
package com.example.config;
import com.example.config.dynamic.DynamicDataSource;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DynamicDataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary // 必须!将动态数据源设置为主数据源
public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 设置默认数据源
return dynamicDataSource;
}
}
注意:这种方式下,SqlSessionFactory
和 TransactionManager
只需要配置一个,并让它们使用这个 @Primary
的 DynamicDataSource
即可。Spring Boot 会自动配置好。
在 Service 方法上添加 @DataSource
注解来切换数据源。
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
// 默认不加注解,使用 master 数据源(因为我们配置了默认值)
@Transactional // 事务仍然有效
public void addProduct(Product product) {
productMapper.insert(product);
}
// 显式指定使用 slave 数据源
@DataSource("slave")
public Product getProductById(Integer id) {
return productMapper.selectById(id);
}
}
DataSourceTransactionManager
与 SqlSessionFactory
绑定的是同一个 DataSource
,@Transactional
注解就能正常工作。在动态方案中,事务管理器绑定的是 DynamicDataSource
,它能确保事务在当前线程选择的数据源上生效。@Transactional
无法管理跨多个数据源的事务。如果你需要在同一个方法中对 master
和 slave
都进行写操作,并保证它们的原子性,你需要引入 JTA(Java Transaction API)事务管理器,例如 Atomikos 或 Narayana。这会增加配置的复杂度。