Java双数据源实战全指南:Spring Boot多数据源配置、原理与踩坑避坑

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。

我们需要做的就是:

  1. 继承 AbstractRoutingDataSource,实现 determineCurrentLookupKey() 方法
  2. 使用 ThreadLocal 存储当前线程的数据源 key,保证线程安全
  3. 通过 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 实战建议

大多数场景下,尽量避免跨数据源事务。可以通过以下方式规避:

  1. 领域拆分:将强关联的表放在同一个数据库,从设计上避免跨库事务
  2. 最终一致性:通过消息队列 + 重试机制实现最终一致,接受短暂的数据不一致
  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 最佳实践

  1. 优先静态分包:能静态配置就不要用动态,简单稳定,调试方便
  2. 避免跨库事务:从设计上规避跨库事务,用最终一致性替代强一致
  3. 做好监控:连接池、慢 SQL、数据源切换都要做好监控
  4. 规范命名:数据源、Mapper、配置类命名要规范,便于维护
  5. 充分测试:多数据源场景复杂,要覆盖各种边界情况的测试

双数据源是企业级开发中的常见需求,理解其底层原理,掌握正确的配置方式,避开常见坑点,才能让系统稳定高效地运行。希望这篇文章能帮你少走弯路,快速落地多数据源方案。


相关推荐
神奇小汤圆1 小时前
Redis主从切换后,旧主恢复竟触发全量同步?——我猜错了,原因比想象中更严谨
后端
JoyT1 小时前
滚动MAD与孤立森林在时序异常检测中的协同应用
后端
程序员cxuan1 小时前
白嫖 Claude Max 20x 漏洞完整事件始末
人工智能·后端·程序员
颜进强1 小时前
LangChain 从入门到实践:用最小案例理解 RAG 的 5 个核心抽象
前端·后端·ai编程
颜进强1 小时前
MCP 从入门到实践:用 TypeScript 实现第一个 MCP Server
前端·后端·ai编程
Undoom1 小时前
用搜索引擎 API 搭一个 SEO 关键词监控工具:定时追踪排名、广告和相关搜索
后端
程序大爆炸2 小时前
gcsfuse的TypeCache
后端
心运软件2 小时前
Python实战:中国大学排行榜数据采集与可视化大屏
后端·python
GISer_Jing2 小时前
前端转全栈须知后端知识
前端·后端·ai·前端框架