Java双数据源实战全指南:Spring Boot多数据源配置、原理与踩坑避坑
一、为什么需要双数据源?
在企业级开发中,单数据源往往无法满足复杂业务场景的需求。随着业务规模扩大,我们经常会遇到以下场景:
- 读写分离:主库负责写入,从库负责查询,提升系统吞吐量
- 多业务库隔离:用户库、订单库、商品库分库存储,避免单库压力过大
- 新旧系统迁移:迁移过程中需要同时访问旧库和新库,平滑过渡
- 跨库数据聚合:需要从多个业务库拉取数据进行统计分析
- 多租户架构:不同租户使用不同的数据库,实现数据隔离
本文将从原理到实战,全面讲解 Java 生态中双数据源/多数据源的三种主流实现方案,附完整可运行代码,并总结高频踩坑点与性能优化建议。
二、核心概念与底层原理
2.1 什么是数据源(DataSource)?
DataSource 是 JDBC 规范中定义的数据库连接工厂,负责管理数据库连接池。相比于传统的 DriverManager,DataSource 具有连接池复用、统一配置管理、事务支持等优势。
在 Spring Boot 中,默认使用 HikariCP 作为连接池实现,它是目前性能最优的 Java 连接池,具有启动快、低延迟、高并发的特点。
2.2 双数据源的核心原理
双数据源的本质是:在 Spring 容器中配置多个 DataSource Bean,通过不同的包路径、注解或动态路由,让不同的 Mapper/Repository 使用对应的数据源。
核心组件包括:
- DataSource:数据库连接池,每个数据源对应一个实例
- SqlSessionFactory(MyBatis):创建 SqlSession,每个数据源对应一个
- EntityManagerFactory(JPA):创建 EntityManager,每个数据源对应一个
- PlatformTransactionManager:事务管理器,每个数据源对应一个
- MapperScannerConfigurer:扫描指定包下的 Mapper,绑定到对应 SqlSessionFactory
2.3 三种实现方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 静态分包配置 | 数据源固定、业务边界清晰 | 配置简单、性能好、事务支持完善 | 扩展性差,新增数据源需改配置 |
| 动态数据源路由 | 需要动态切换、多租户场景 | 灵活、可动态扩展、代码侵入小 | 事务处理复杂、存在线程安全问题 |
| ShardingSphere 分库分表 | 大规模分库分表、读写分离 | 功能强大、支持分布式事务 | 配置复杂、学习成本高 |
三、方案一:静态分包双数据源配置(MyBatis 版)
这是最常用、最稳定的方案。通过将不同的 Mapper 放在不同的包下,每个包绑定对应的数据源,实现数据源隔离。
3.1 项目结构
bash
src/main/java/com/example/demo
├── config # 数据源配置类
├── mapper
│ ├── master # 主库 Mapper
│ └── slave # 从库 Mapper
├── entity # 实体类
├── service # 业务层
└── controller # 控制层
3.2 引入依赖
xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
3.3 配置文件
yaml
spring:
datasource:
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
jdbc-url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
注意: 多数据源配置时必须使用 jdbc-url 而不是 url,否则 HikariCP 无法正确识别配置。
3.4 主数据源配置类
less
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.master",
sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {
@Bean(name = "masterDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "masterSqlSessionFactory")
@Primary
public SqlSessionFactory masterSqlSessionFactory(
@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 配置 Mapper 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);
}
}
3.5 从数据源配置类
less
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.slave",
sqlSessionFactoryRef = "slaveSqlSessionFactory")
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 {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
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);
}
}
关键要点:
- 必须有且仅有一个数据源标注
@Primary,否则 Spring 会因找到多个 Bean 而报错 - 每个数据源对应独立的 SqlSessionFactory 和 TransactionManager
- 通过
@MapperScan的 basePackages 指定扫描范围,实现包级别的数据源绑定
3.6 使用方式
typescript
@Service
public class UserService {
@Autowired
private UserMasterMapper userMasterMapper; // 自动注入主库 Mapper
@Autowired
private UserSlaveMapper userSlaveMapper; // 自动注入从库 Mapper
public void addUser(User user) {
userMasterMapper.insert(user); // 写入主库
}
public List<User> listUsers() {
return userSlaveMapper.selectAll(); // 从从库查询
}
}
四、方案二:动态数据源路由(AbstractRoutingDataSource)
当需要在运行时动态切换数据源时(比如多租户场景、按请求路由不同数据源),静态分包就不够灵活了。Spring 提供了 AbstractRoutingDataSource 抽象类,可以实现动态数据源路由。
4.1 实现原理
AbstractRoutingDataSource 的核心逻辑是:在获取连接时,通过 determineCurrentLookupKey() 方法获取当前数据源的 key,然后从目标数据源 Map 中找到对应的 DataSource。
我们需要做的就是:
- 继承 AbstractRoutingDataSource,实现 determineCurrentLookupKey() 方法
- 使用 ThreadLocal 存储当前线程的数据源 key,保证线程安全
- 通过 AOP + 自定义注解,在方法执行前切换数据源
4.2 数据源上下文持有器
typescript
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
4.3 动态数据源实现类
scala
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
4.4 动态数据源配置类
typescript
@Configuration
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource() {
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;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource());
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
return bean.getObject();
}
}
4.5 自定义注解 + AOP 实现自动切换
less
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "master";
}
less
@Aspect
@Component
@Order(-1) // 保证在事务切面之前执行
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void beforeSwitchDataSource(JoinPoint point, DataSource dataSource) {
String dataSourceName = dataSource.value();
DataSourceContextHolder.setDataSource(dataSourceName);
}
@After("@annotation(dataSource)")
public void afterSwitchDataSource(JoinPoint point, DataSource dataSource) {
DataSourceContextHolder.clearDataSource();
}
}
4.6 使用方式
typescript
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@DataSource("master") // 使用主库
public void addUser(User user) {
userMapper.insert(user);
}
@DataSource("slave") // 使用从库
public List<User> listUsers() {
return userMapper.selectAll();
}
}
重要坑点: 动态数据源与事务结合时必须注意:
- 事务开启后无法切换数据源,因为 Connection 已经被事务持有
- 必须保证数据源切换在事务开启之前执行(AOP 顺序要正确)
- 跨数据源事务需要使用分布式事务(如 Atomikos、Seata)
五、方案三:Spring Data JPA 双数据源配置
如果你使用的是 JPA 而不是 MyBatis,配置思路类似,只是把 SqlSessionFactory 换成 EntityManagerFactory。
5.1 主数据源配置
less
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager",
basePackages = "com.example.demo.repository.primary"
)
public class PrimaryDataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "primaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("primaryDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.example.demo.entity.primary")
.persistenceUnit("primary")
.build();
}
@Primary
@Bean(name = "primaryTransactionManager")
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
return new JpaTransactionManager(factory);
}
}
六、跨数据源事务处理
6.1 问题场景
当一个业务方法需要同时操作两个数据源,并且要求原子性时(要么都成功,要么都回滚),普通的本地事务就无能为力了。这时候需要分布式事务。
6.2 解决方案
| 方案 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|
| XA 两阶段提交(Atomikos/Bitronix) | 强一致 | 低 | 对一致性要求极高、并发不高 |
| Seata AT 模式 | 最终一致 | 中 | 大多数业务场景,性能与一致性平衡 |
| TCC 模式 | 最终一致 | 高 | 高并发场景,代码侵入大 |
| 本地消息表 + 重试 | 最终一致 | 高 | 对一致性要求不高,允许短暂不一致 |
6.3 实战建议
大多数场景下,尽量避免跨数据源事务。可以通过以下方式规避:
- 领域拆分:将强关联的表放在同一个数据库,从设计上避免跨库事务
- 最终一致性:通过消息队列 + 重试机制实现最终一致,接受短暂的数据不一致
- 分布式事务框架:确实需要时,推荐使用 Seata,社区活跃、文档完善、接入成本低
七、高频踩坑点与解决方案
7.1 坑点一:配置文件用 url 而不是 jdbc-url
现象:启动报错 "jdbcUrl is required with driverClassName"
原因:Spring Boot 单数据源时自动识别 url,但多数据源手动创建 DataSource 时,HikariCP 需要 jdbc-url 属性。
解决 :配置文件中使用 jdbc-url 而不是 url。
7.2 坑点二:缺少 @Primary 注解
现象:启动报错 "No qualifying bean of type 'javax.sql.DataSource' available: expected single matching bean but found 2"
原因:Spring 容器中存在多个 DataSource Bean,不知道注入哪一个。
解决 :给其中一个 DataSource 加上 @Primary 注解,作为默认注入的数据源。
7.3 坑点三:动态数据源切换不生效
现象:加了 @DataSource 注解,但还是用的默认数据源
原因:
- AOP 切面没有生效(比如切面类没加 @Component)
- 方法是 private 的,AOP 无法拦截
- 同类方法调用,绕过了代理对象
- 事务先于数据源切换执行
解决:
- 确保切面类被 Spring 管理
- 切换数据源的方法必须是 public 的
- 同类调用通过注入自身或使用 AopContext.currentProxy()
- 设置切面 Order 为 -1,保证在事务切面之前执行
7.4 坑点四:ThreadLocal 数据源泄漏
现象</b:高并发下出现数据源错乱,A 用户请求走到了 B 用户的数据源
原因:使用线程池时,线程复用导致 ThreadLocal 中的数据没有被清理。
解决 :在方法执行后(@After 或 finally 中)调用 DataSourceContextHolder.clearDataSource() 清理 ThreadLocal。
7.5 坑点五:事务内切换数据源失效
现象:在 @Transactional 方法内切换数据源,但没有生效
原因:事务开启时就已经获取了 Connection 并绑定到线程,后续切换数据源不会改变已有的 Connection。
解决:
- 在事务开启之前切换数据源(保证 AOP 顺序正确)
- 使用 REQUIRES_NEW 传播级别,开启新事务
- 确实需要跨库事务时,使用分布式事务方案
7.6 坑点六:Mapper XML 路径扫描不到
现象:启动报错 "Invalid bound statement (not found)"
原因:多数据源配置时,每个 SqlSessionFactory 只扫描了自己目录下的 XML 文件,路径配置错误。
解决 :检查 setMapperLocations 的路径配置,确保 XML 文件放在正确的目录下。
7.7 坑点七:分页插件不生效
现象:配置了 PageHelper 或 MyBatis-Plus 分页插件,但分页不生效
原因:多数据源时,分页插件需要配置到每个 SqlSessionFactory 中。
解决:在每个 SqlSessionFactory 中手动添加分页插件:
ini
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 添加分页插件
Interceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("helperDialect", "mysql");
interceptor.setProperties(properties);
bean.setPlugins(new Interceptor[]{interceptor});
八、性能优化建议
8.1 连接池参数优化
合理的连接池配置是性能的关键。以下是 HikariCP 的核心参数建议:
yaml
spring:
datasource:
hikari:
maximum-pool-size: 20 # 最大连接数,建议 CPU 核心数 * 2 + 磁盘数
minimum-idle: 5 # 最小空闲连接数
connection-timeout: 30000 # 获取连接超时时间(毫秒)
idle-timeout: 600000 # 空闲连接超时时间(毫秒)
max-lifetime: 1800000 # 连接最大存活时间(毫秒)
connection-test-query: SELECT 1 # 连接测试查询
8.2 读写分离优化
- 主库只负责写:所有写操作走主库,读操作走从库
- 强制走主库:对于实时性要求高的查询(如刚写完就查),强制走主库
- 一主多从:读压力大时,配置多个从库,通过负载均衡算法分发请求
8.3 其他优化建议
- 懒加载:动态数据源支持懒加载,只有真正使用时才创建连接
- 监控告警:接入 Druid 或 Micrometer 监控连接池状态,设置告警阈值
- 慢 SQL 优化:开启慢 SQL 日志,定期优化慢查询
- 合理设置事务范围:事务尽可能小,避免长时间占用连接
九、总结与选型建议
9.1 方案选型
| 业务场景 | 推荐方案 |
|---|---|
| 业务边界清晰、数据源固定 | 静态分包配置(简单、稳定、性能好) |
| 多租户、按请求动态路由 | 动态数据源路由(灵活、可扩展) |
| 大规模分库分表、读写分离 | ShardingSphere(功能强大、生态完善) |
| 跨库事务、强一致性要求 | Seata 分布式事务 |
9.2 最佳实践
- 优先静态分包:能静态配置就不要用动态,简单稳定,调试方便
- 避免跨库事务:从设计上规避跨库事务,用最终一致性替代强一致
- 做好监控:连接池、慢 SQL、数据源切换都要做好监控
- 规范命名:数据源、Mapper、配置类命名要规范,便于维护
- 充分测试:多数据源场景复杂,要覆盖各种边界情况的测试
双数据源是企业级开发中的常见需求,理解其底层原理,掌握正确的配置方式,避开常见坑点,才能让系统稳定高效地运行。希望这篇文章能帮你少走弯路,快速落地多数据源方案。