
Java双数据源实战全指南:Spring Boot多数据源配置、原理与踩坑避坑
在企业级开发中,单数据源往往无法满足复杂业务场景的需求。随着业务规模扩大,我们经常会遇到以下场景:
本文将从原理到实战,全面讲解 Java 生态中双数据源/多数据源的三种主流实现方案,附完整可运行代码,并总结高频踩坑点与性能优化建议。
DataSource 是 JDBC 规范中定义的数据库连接工厂,负责管理数据库连接池。相比于传统的 DriverManager,DataSource 具有连接池复用、统一配置管理、事务支持等优势。
在 Spring Boot 中,默认使用 HikariCP 作为连接池实现,它是目前性能最优的 Java 连接池,具有启动快、低延迟、高并发的特点。
双数据源的本质是:在 Spring 容器中配置多个 DataSource Bean,通过不同的包路径、注解或动态路由,让不同的 Mapper/Repository 使用对应的数据源。
核心组件包括:
方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
静态分包配置 | 数据源固定、业务边界清晰 | 配置简单、性能好、事务支持完善 | 扩展性差,新增数据源需改配置 |
动态数据源路由 | 需要动态切换、多租户场景 | 灵活、可动态扩展、代码侵入小 | 事务处理复杂、存在线程安全问题 |
ShardingSphere 分库分表 | 大规模分库分表、读写分离 | 功能强大、支持分布式事务 | 配置复杂、学习成本高 |
这是最常用、最稳定的方案。通过将不同的 Mapper 放在不同的包下,每个包绑定对应的数据源,实现数据源隔离。
src/main/java/com/example/demo
├── config # 数据源配置类
├── mapper
│ ├── master # 主库 Mapper
│ └── slave # 从库 Mapper
├── entity # 实体类
├── service # 业务层
└── controller # 控制层<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>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 无法正确识别配置。
@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);
}
}@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 而报错@MapperScan 的 basePackages 指定扫描范围,实现包级别的数据源绑定@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(); // 从从库查询
}
}当需要在运行时动态切换数据源时(比如多租户场景、按请求路由不同数据源),静态分包就不够灵活了。Spring 提供了 AbstractRoutingDataSource 抽象类,可以实现动态数据源路由。
AbstractRoutingDataSource 的核心逻辑是:在获取连接时,通过 determineCurrentLookupKey() 方法获取当前数据源的 key,然后从目标数据源 Map 中找到对应的 DataSource。
我们需要做的就是:
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();
}
}public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}@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();
}
}@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "master";
}@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();
}
}@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();
}
}重要坑点:动态数据源与事务结合时必须注意:
如果你使用的是 JPA 而不是 MyBatis,配置思路类似,只是把 SqlSessionFactory 换成 EntityManagerFactory。
@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);
}
}当一个业务方法需要同时操作两个数据源,并且要求原子性时(要么都成功,要么都回滚),普通的本地事务就无能为力了。这时候需要分布式事务。
方案 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|
XA 两阶段提交(Atomikos/Bitronix) | 强一致 | 低 | 对一致性要求极高、并发不高 |
Seata AT 模式 | 最终一致 | 中 | 大多数业务场景,性能与一致性平衡 |
TCC 模式 | 最终一致 | 高 | 高并发场景,代码侵入大 |
本地消息表 + 重试 | 最终一致 | 高 | 对一致性要求不高,允许短暂不一致 |
大多数场景下,尽量避免跨数据源事务。可以通过以下方式规避:
现象:启动报错 "jdbcUrl is required with driverClassName"
原因:Spring Boot 单数据源时自动识别 url,但多数据源手动创建 DataSource 时,HikariCP 需要 jdbc-url 属性。
解决:配置文件中使用 jdbc-url 而不是 url。
现象:启动报错 "No qualifying bean of type 'javax.sql.DataSource' available: expected single matching bean but found 2"
原因:Spring 容器中存在多个 DataSource Bean,不知道注入哪一个。
解决:给其中一个 DataSource 加上 @Primary 注解,作为默认注入的数据源。
现象:加了 @DataSource 注解,但还是用的默认数据源
原因:
解决:
现象</b:高并发下出现数据源错乱,A 用户请求走到了 B 用户的数据源
原因:使用线程池时,线程复用导致 ThreadLocal 中的数据没有被清理。
解决:在方法执行后(@After 或 finally 中)调用 DataSourceContextHolder.clearDataSource() 清理 ThreadLocal。
现象:在 @Transactional 方法内切换数据源,但没有生效
原因:事务开启时就已经获取了 Connection 并绑定到线程,后续切换数据源不会改变已有的 Connection。
解决:
现象:启动报错 "Invalid bound statement (not found)"
原因:多数据源配置时,每个 SqlSessionFactory 只扫描了自己目录下的 XML 文件,路径配置错误。
解决:检查 setMapperLocations 的路径配置,确保 XML 文件放在正确的目录下。
现象:配置了 PageHelper 或 MyBatis-Plus 分页插件,但分页不生效
原因:多数据源时,分页插件需要配置到每个 SqlSessionFactory 中。
解决:在每个 SqlSessionFactory 中手动添加分页插件:
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});合理的连接池配置是性能的关键。以下是 HikariCP 的核心参数建议:
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 # 连接测试查询业务场景 | 推荐方案 |
|---|---|
业务边界清晰、数据源固定 | 静态分包配置(简单、稳定、性能好) |
多租户、按请求动态路由 | 动态数据源路由(灵活、可扩展) |
大规模分库分表、读写分离 | ShardingSphere(功能强大、生态完善) |
跨库事务、强一致性要求 | Seata 分布式事务 |
双数据源是企业级开发中的常见需求,理解其底层原理,掌握正确的配置方式,避开常见坑点,才能让系统稳定高效地运行。希望这篇文章能帮你少走弯路,快速落地多数据源方案。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。