在 Spring Boot 中配置多数据源是一个非常常见的需求,主要用于以下场景:
- 读写分离:一个主数据库(Master)负责写操作,一个或多个从数据库(Slave)负责读操作,以提高性能和可用性。
- 业务拆分:不同的业务模块使用不同的数据库(例如,用户库、订单库、商品库)。
- 连接异构数据库:同时连接 MySQL、PostgreSQL 等不同类型的数据库。
下面我将详细介绍两种主流的实现方式:
- 静态方式(推荐用于业务隔离场景):通过包路径区分不同的数据源,配置简单,结构清晰。
- 动态方式(推荐用于读写分离场景):使用 AOP 和自定义注解,在方法级别动态切换数据源,更灵活。
方案一:静态方式(按包路径隔离)
这种方式的核心思想是为每个数据源创建一套独立的配置(DataSource
, SqlSessionFactory
, TransactionManager
),并使用 @MapperScan
注解扫描不同包路径下的 Mapper 接口,将它们绑定到对应的数据源上。
1. 添加依赖 (pom.xml)
确保有以下依赖。通常 Spring Boot Starter 会包含大部分。
<dependencies><!-- Spring Boot Web Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- MyBatis-Plus Starter --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version> <!-- 请使用较新版本 --></dependency><!-- MySQL Driver --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- Connection Pool (HikariCP is default) --><dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId></dependency>
</dependencies>
2. 配置文件 (application.yml)
为不同的数据源定义各自的连接信息,并用不同的前缀区分。
spring:datasource:# 主数据源配置 (master)master:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/db_master?serverTimezone=UTCusername: rootpassword: your_passwordtype: com.zaxxer.hikari.HikariDataSource # 指定连接池类型# 从数据源配置 (slave)slave:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3307/db_slave?serverTimezone=UTCusername: rootpassword: your_passwordtype: com.zaxxer.hikari.HikariDataSource
3. 创建数据源配置类
为每个数据源创建一个 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")@Primarypublic 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")@Primarypublic DataSourceTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Bean(name = "masterSqlSessionTemplate")@Primarypublic 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);}
}
4. 创建 Mapper 接口
将不同数据源的 Mapper 接口放到各自的包下。
com.example.mapper.master
->UserMasterMapper.java
com.example.mapper.slave
->OrderSlaveMapper.java
5. 使用
现在你可以在 Service 中直接注入并使用对应的 Mapper,Spring 会自动为它们关联正确的数据源。
@Service
public class MyService {@Autowiredprivate UserMasterMapper userMasterMapper; // 操作 master 库@Autowiredprivate OrderSlaveMapper orderSlaveMapper; // 操作 slave 库public void doSomething() {// ...userMasterMapper.insert(someUser); // 写入主库Order order = orderSlaveMapper.selectById(1); // 从从库读取}
}
优点:配置隔离,结构非常清晰,不会混淆。
缺点:如果一个 Service 方法需要同时操作两个库,代码会稍微复杂,且默认的 @Transactional
不能跨数据源生效。
方案二:动态方式(AOP + 自定义注解)
这种方式更灵活,适用于读写分离等需要在同一个 Service 中切换数据源的场景。
1. 配置文件 (application.yml)
与方案一相同。
2. 创建自定义注解
创建一个注解,用于标记方法应该使用哪个数据源。
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 数据源
}
3. 创建数据源上下文持有者
使用 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();}
}
4. 创建动态数据源类
继承 AbstractRoutingDataSource
,重写 determineCurrentLookupKey
方法,从 DataSourceContextHolder
获取当前数据源 Key。
package com.example.config.dynamic;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContextHolder.getDataSourceKey();}
}
5. 创建AOP切面
创建一个切面,拦截 @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();}}
}
6. 整合数据源配置
创建一个统一的配置类来管理所有数据源。
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 会自动配置好。
7. 使用
在 Service 方法上添加 @DataSource
注解来切换数据源。
@Service
public class ProductService {@Autowiredprivate 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。这会增加配置的复杂度。
总结
- 如果你的业务模块和数据库绑定关系固定,方案一(静态方式) 更简单、更直观。
- 如果你需要实现读写分离,或者在业务逻辑中频繁切换数据源,方案二(动态方式) 提供了更高的灵活性。