java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java双数据源/多数据源实现

Java生态中双数据源/多数据源的三种主流实现方案

作者:回家路上绕了弯

在企业级开发中,单数据源往往无法满足复杂业务场景的需求,本文将从原理到实战,全面讲解 Java 生态中双数据源/多数据源的三种主流实现方案,附完整可运行代码,并总结高频踩坑点与性能优化建议,需要的朋友可以参考下

一、为什么需要双数据源?

在企业级开发中,单数据源往往无法满足复杂业务场景的需求。随着业务规模扩大,我们经常会遇到以下场景:

本文将从原理到实战,全面讲解 Java 生态中双数据源/多数据源的三种主流实现方案,附完整可运行代码,并总结高频踩坑点与性能优化建议。

二、核心概念与底层原理

2.1 什么是数据源(DataSource)?

DataSource 是 JDBC 规范中定义的数据库连接工厂,负责管理数据库连接池。相比于传统的 DriverManager,DataSource 具有连接池复用、统一配置管理、事务支持等优势。

在 Spring Boot 中,默认使用 HikariCP 作为连接池实现,它是目前性能最优的 Java 连接池,具有启动快、低延迟、高并发的特点。

2.2 双数据源的核心原理

双数据源的本质是:在 Spring 容器中配置多个 DataSource Bean,通过不同的包路径、注解或动态路由,让不同的 Mapper/Repository 使用对应的数据源

核心组件包括:

2.3 三种实现方案对比

方案适用场景优点缺点
静态分包配置数据源固定、业务边界清晰配置简单、性能好、事务支持完善扩展性差,新增数据源需改配置
动态数据源路由需要动态切换、多租户场景灵活、可动态扩展、代码侵入小事务处理复杂、存在线程安全问题
ShardingSphere 分库分表大规模分库分表、读写分离功能强大、支持分布式事务配置复杂、学习成本高

三、方案一:静态分包双数据源配置(MyBatis 版)

这是最常用、最稳定的方案。通过将不同的 Mapper 放在不同的包下,每个包绑定对应的数据源,实现数据源隔离。

3.1 项目结构

src/main/java/com/example/demo
├── config          # 数据源配置类
├── mapper
│   ├── master      # 主库 Mapper
│   └── slave       # 从库 Mapper
├── entity          # 实体类
├── service         # 业务层
└── controller      # 控制层

3.2 引入依赖

<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 配置文件

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 主数据源配置类

@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 从数据源配置类

@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);
    }
}

关键要点:

3.6 使用方式

@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 数据源上下文持有器

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 动态数据源实现类

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

4.4 动态数据源配置类

@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 实现自动切换

@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();
    }
}

4.6 使用方式

@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();
    }
}

重要坑点: 动态数据源与事务结合时必须注意:

五、方案三:Spring Data JPA 双数据源配置

如果你使用的是 JPA 而不是 MyBatis,配置思路类似,只是把 SqlSessionFactory 换成 EntityManagerFactory。

5.1 主数据源配置

@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 注解,但还是用的默认数据源

原因

解决

7.4 坑点四:ThreadLocal 数据源泄漏

现象</b:高并发下出现数据源错乱,A 用户请求走到了 B 用户的数据源

原因:使用线程池时,线程复用导致 ThreadLocal 中的数据没有被清理。

解决:在方法执行后(@After 或 finally 中)调用 DataSourceContextHolder.clearDataSource() 清理 ThreadLocal。

7.5 坑点五:事务内切换数据源失效

现象:在 @Transactional 方法内切换数据源,但没有生效

原因:事务开启时就已经获取了 Connection 并绑定到线程,后续切换数据源不会改变已有的 Connection。

解决

7.6 坑点六:Mapper XML 路径扫描不到

现象:启动报错 "Invalid bound statement (not found)"

原因:多数据源配置时,每个 SqlSessionFactory 只扫描了自己目录下的 XML 文件,路径配置错误。

解决:检查 setMapperLocations 的路径配置,确保 XML 文件放在正确的目录下。

7.7 坑点七:分页插件不生效

现象:配置了 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});

八、性能优化建议

8.1 连接池参数优化

合理的连接池配置是性能的关键。以下是 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  # 连接测试查询

8.2 读写分离优化

8.3 其他优化建议

九、总结与选型建议

9.1 方案选型

业务场景推荐方案
业务边界清晰、数据源固定静态分包配置(简单、稳定、性能好)
多租户、按请求动态路由动态数据源路由(灵活、可扩展)
大规模分库分表、读写分离ShardingSphere(功能强大、生态完善)
跨库事务、强一致性要求Seata 分布式事务

9.2 最佳实践

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

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

以上就是Java生态中双数据源/多数据源的三种主流实现方案的详细内容,更多关于Java双数据源/多数据源实现的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文