SpringBoot多数据源配置详解

发布于:2025-06-24 ⋅ 阅读:(15) ⋅ 点赞:(0)

多数据源核心概念

多数据源是指在一个应用程序中同时连接和使用多个数据库的能力。在实际开发中,我们经常会遇到以下场景需要多数据源:

  • 同时连接生产数据库和报表数据库
  • 读写分离场景(主库写,从库读)
  • 微服务架构中需要访问其他服务的数据库
  • 多租户系统中每个租户有独立数据库

多数据源实现示例

多数据源的配置文件以及配置类

  • application.yml 配置示例

    spring:
      datasource:
          jdbc-url: jdbc:mysql://localhost:3306/db1 # 主数据源
          username: root
          password: root123
          driver-class-name: com.mysql.cj.jdbc.Driver
          hikari:
            pool-name: PrimaryHikariPool
            # 最大连接数 
            maximum-pool-size: 20
            # 最小空闲连接
            minimum-idle: 5
            # 空闲连接超时时间(ms)
            idle-timeout: 30000
            # 连接最大生命周期(ms)
            max-lifetime: 1800000
            # 获取连接超时时间(ms)
            connection-timeout: 30000
            connection-test-query: SELECT 1
      second-datasource:
          jdbc-url: jdbc:mysql://localhost:3306/db2 # 主数据源
          username: root
          password: root123
          driver-class-name: com.mysql.cj.jdbc.Driver
          hikari:
            pool-name: SecondHikariPool
            maximum-pool-size: 20
            minimum-idle: 5
            idle-timeout: 30000
            max-lifetime: 1800000
            connection-timeout: 30000
            connection-test-query: SELECT 1
    
  • 多数据源配置类

    import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import javax.sql.DataSource;
    
    @Configuration
    public class DbConfig {
    
        @Bean("db1DataSourceProperties")
        @ConfigurationProperties(prefix = "spring.datasource")
        public DataSourceProperties db1DataSourceProperties() {
            return new DataSourceProperties();
        }
    
        @Bean(name = "db1DataSource")
        public DataSource dataSource() {
            return db1DataSourceProperties().initializeDataSourceBuilder().build();
        }
    
        
        @Bean("db2DataSourceProperties")
        @ConfigurationProperties(prefix = "spring.second-datasource")
        public DataSourceProperties db2DataSourceProperties() {
            return new DataSourceProperties();
        }
    
        @Bean(name = "db2DataSource")
        public DataSource db2DataSource() {
            return db2DataSourceProperties().initializeDataSourceBuilder().build();
        }
    }
    
  • 禁用默认数据源

    多数据源时需在主类排除自动配置

    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 
    public class App {
        public static void main(String[] args) {
            SpringApplication.run(App.class, args);
        }
    }
    

JPA 多数据源配置

  • 主数据源 JAP 配置

    import com.querydsl.jpa.impl.JPAQueryFactory;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
    import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.orm.jpa.JpaTransactionManager;
    import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
    import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    import javax.persistence.EntityManager;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Objects;
    
    @Configuration
    // 启用 Spring 的事务管理功能,允许使用 @Transactional 注解来管理事务
    @EnableTransactionManagement
    // 启用 JPA 仓库的自动扫描和注册功能
    @EnableJpaRepositories(
            // 指定要扫描的 JPA 仓库接口所在的包路径
            basePackages = "com.example.db1",
            // 指定使用的实体管理器工厂的 Bean 名称
            entityManagerFactoryRef = "db1EntityManagerFactory",
            // 指定使用的事务管理器的 Bean 名称
            transactionManagerRef = "db1TransactionManager"
    )
    public class Db1JpaConfig {
        /**
         * 创建实体管理器工厂的 Bean,并将其标记为主要的实体管理器工厂 Bean
         */
        @Bean(name = "db1EntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean entityManagerFactory(
                @Qualifier("db1DataSource")DataSource dataSource,
                JpaProperties jpaProperties) {
            return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), 
                                                   new HashMap<>(), null)
                    // 设置数据源
                    .dataSource(dataSource)
                    // 指定要扫描的实体类所在的包路径
                    .packages("com.example.db1")
                    // 设置持久化单元的名称
                    .persistenceUnit("db1")
                    // 设置 JPA 的属性
                    .properties(jpaProperties.getProperties())
                    .build();
        }
    
        /**
         * 创建事务管理器的 Bean,并将其标记为主要的事务管理器 Bean
         */
        @Bean(name = "db1TransactionManager")
        public PlatformTransactionManager transactionManager(
                @Qualifier("db1EntityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
            return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactory.getObject()));
        }
    
        /**
         * QueryDSL的核心组件
         */
        @Bean(name = "db1JPAQueryFactory")
        public JPAQueryFactory db1JPAQueryFactory(
                @Qualifier("db1EntityManagerFactory") EntityManager entityManager) {
            return new JPAQueryFactory(entityManager);
        }
    }
    
    
  • 从数据源 JAP 集成配置(略)


MyBatis 多数据源配置

  • 主数据源 MyBatis 配置

    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import javax.sql.DataSource;
    
    @Configuration
    // 此注解用于指定 MyBatis Mapper 接口的扫描范围和对应的 SqlSessionFactory 引用
    @MapperScan(
            // 指定要扫描的 Mapper 接口所在的基础包路径
            basePackages = "com.example.mapper.db1",
            // 配置使用的 SqlSessionFactory Bean 的名称
            sqlSessionFactoryRef = "db1SqlSessionFactory"
    )
    public class Db1MyBatisConfig {
    
        /**
         * 创建 SqlSessionFactory Bean
         */
        @Bean("db1SqlSessionFactory")
        public SqlSessionFactory db1SqlSessionFactory(
                @Qualifier("db1DataSource") DataSource dataSource) throws Exception {
            // 创建 SqlSessionFactoryBean 实例,用于创建 SqlSessionFactory
            SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
            // 设置 SqlSessionFactory 使用的数据源
            sessionFactory.setDataSource(dataSource);
            // 设置 Mapper XML 文件的位置,使用 PathMatchingResourcePatternResolver 来查找匹配的资源
            sessionFactory.setMapperLocations(
                    new PathMatchingResourcePatternResolver()
                            .getResources("classpath:mapper/db1/*.xml"));
            // 获取并返回 SqlSessionFactory 实例
            return sessionFactory.getObject();
        }
    
        /**
         * 创建 SqlSessionTemplate Bean
         */
        @Bean("db1SqlSessionTemplate")
        public SqlSessionTemplate db1SqlSessionTemplate(
                @Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            // 创建并返回 SqlSessionTemplate 实例,用于简化 MyBatis 的操作
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    
        /**
         * 创建事务管理器的 Bean,并将其标记为主要的事务管理器 Bean
         */
        @Bean("db1TransactionManager")
        public PlatformTransactionManager transactionManager(
                @Qualifier("db1DataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
    
  • 从数据源 MyBatis 配置(略)


事务管理:跨数据源事务处理

单数据源事务

  • 在单数据源场景下,Spring的事务管理非常简单:

    @Service
    public class AccountService {
    
        @Transactional  // 使用默认事务管理器
        public void transfer(Long fromId, Long toId, BigDecimal amount) {
            // do some thing ...
        }
    }
    

多数据源事务挑战

多数据源事务面临的主要问题是分布式事务的挑战。Spring 的 @Transactional 注解默认只能管理单个事务管理器,无法直接协调多个数据源的事务。

解决方案对比:

方案 原理 优点 缺点 适用场景
JTA (Java Transaction API) 使用全局事务协调器 强一致性 性能开销大,配置复杂 需要强一致性的金融系统
最终一致性 (Saga模式) 通过补偿操作实现 高性能,松耦合 实现复杂,需要补偿逻辑 高并发,可接受短暂不一致
本地消息表 通过消息队列保证 可靠性高 需要额外表存储消息 需要可靠异步处理的场景

事务管理器:DataSourceTransactionManager 和 JpaTransactionManager

DataSourceTransactionManager 和 JpaTransactionManager 是 Spring 框架中针对不同持久层技术的事务管理器。

技术栈适配差异

  • DataSourceTransactionManager

    • 适用场景:纯 JDBC、MyBatis、JdbcTemplate 等基于原生 SQL 的数据访问技术
    • 事务控制对象:直接管理 java.sql.Connection ,通过数据库连接实现事务
    • 局限性:
      • 无法自动绑定 JPA 或 Hibernate 的 EntityManager/Session 到当前事务上下文
      • 混合使用 JDBC 和 JPA 时可能导致连接隔离(各自使用独立连接),破坏事务一致性
  • JpaTransactionManager

    • 适用场景:JPA 规范实现(如 Hibernate、EclipseLink)
    • 事务控制对象:管理 JPA EntityManager,通过其底层连接协调事务
    • 核心优势:
      • 自动将 EntityManager 绑定到线程上下文,确保同一事务中多次操作使用同一连接
      • 支持 JPA 的延迟加载(Lazy Loading)、缓存同步等特性
  • 混合技术栈的特殊情况

    • 混合技术栈需严格隔离事务管理器,并考虑分布式事务需求

      JPA操作使用JpaTransactionManager,MyBatis操作使用DataSourceTransactionManager

      跨数据源事务需引入分布式事务(如Atomikos),否则不同数据源的事务无法保证原子性

    • 若一个 Service 方法同时使用 JPA和 Mybatis(未验证):

      • 使用 DataSourceTransactionManager 可能导致两个操作使用不同连接,违反 ACID
      • 使用 JpaTransactionManager 能保证两者共享同一连接(因 JPA 底层复用 DataSource 连接)

事务同步机制对比

特性 DataSourceTransactionManager JpaTransactionManager
连接资源管理 直接管理 Connection 通过 EntityManager 间接管理连接
跨技术兼容性 仅限 JDBC 系技术 支持 JPA 及其混合场景(如 JPA+JDBC)
高级 ORM 功能支持 不支持(如延迟加载) 完整支持 JPA 特性
配置复杂度 简单(仅需 DataSource) 需额外配置 EntityManagerFactory

多数据源事务使用

  • 事务配置详见上文

  • 多数据源事务使用示例

    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class AccountService {
        
        @Transactional(transactionManager = "db1TransactionManager")  // 指定事务管理器
        public void transfer(Long fromId, Long toId, BigDecimal amount) {
            // do some thing ...
        }
    }
    

基于 AbstractRoutingDataSource 的动态数据源

  • 动态数据源上下文

    public class DynamicDataSourceContextHolder {
        // 使用ThreadLocal保证线程安全
        private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    
        // 数据源列表
        public static final String PRIMARY_DS = "primary";
        public static final String SECONDARY_DS = "secondary";
    
        public static void setDataSourceType(String dsType) {
            CONTEXT_HOLDER.set(dsType);
        }
    
        public static String getDataSourceType() {
            return CONTEXT_HOLDER.get();
        }
    
        public static void clearDataSourceType() {
            CONTEXT_HOLDER.remove();
        }
    }
    
  • 动态数据源配置

    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    @Configuration
    public class DynamicDataSourceConfig {
    
        /**
         * 创建动态数据源 Bean,并将其设置为主要的数据源 Bean
         */
        @Bean
        @Primary
        public DataSource dynamicDataSource(
                @Qualifier("db1DataSource") DataSource db1DataSource,
                @Qualifier("db2DataSource") DataSource db2DataSource) {
            // 用于存储目标数据源的映射,键为数据源标识,值为数据源实例
            Map<Object, Object> targetDataSources = new HashMap<>();
            // 将主数据源添加到目标数据源映射中,使用自定义的主数据源标识
            targetDataSources.put(DynamicDataSourceContextHolder.PRIMARY_DS, db1DataSource);
            // 将从数据源添加到目标数据源映射中,使用自定义的从数据源标识
            targetDataSources.put(DynamicDataSourceContextHolder.SECONDARY_DS, db2DataSource);
    
            // 创建自定义的动态数据源实例
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            // 设置动态数据源的目标数据源映射
            dynamicDataSource.setTargetDataSources(targetDataSources);
            // 设置动态数据源的默认目标数据源为主数据源
            dynamicDataSource.setDefaultTargetDataSource(db1DataSource);
    
            return dynamicDataSource;
        }
    
        /**
         * 自定义动态数据源类,继承自 AbstractRoutingDataSource
         */
        private static class DynamicDataSource extends AbstractRoutingDataSource {
            /**
             * 确定当前要使用的数据源的标识
             * @return 当前数据源的标识
             */
            @Override
            protected Object determineCurrentLookupKey() {
                // 从上下文持有者中获取当前要使用的数据源类型
                return DynamicDataSourceContextHolder.getDataSourceType();
            }
        }
    }
    
  • 基于AOP的读写分离实现

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface ReadOnly {
        // 标记为读操作
    }
    
    @Aspect
    @Component
    public class ReadWriteDataSourceAspect {
        
        @Before("@annotation(readOnly)")
        public void beforeSwitchDataSource(JoinPoint point, ReadOnly readOnly) {
            DynamicDataSourceContextHolder.setDataSourceType(DynamicDataSourceContextHolder.SECONDARY_DS);
        }
        
        @After("@annotation(readOnly)")
        public void afterSwitchDataSource(JoinPoint point, ReadOnly readOnly) {
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }
    
  • 使用示例

    @Service
    public class ProductService {
        
        @Autowired
        private ProductRepository productRepository;
        
        @Transactional
        public void createProduct(Product product) {
            // 默认使用主数据源(写)
            productRepository.save(product);
        }
        
        @ReadOnly  // 执行该注解标记的方法时,前后都会执行ReadWriteDataSourceAspect切面类方法
        @Transactional
        public Product getProduct(Long id) {
            // 使用从数据源(读)
            return productRepository.findById(id).orElse(null);
        }
        
        @ReadOnly
        @Transactional
        public List<Product> listProducts() {
            // 使用从数据源(读)
            return productRepository.findAll();
        }
    }
    

常见问题与解决方案

典型问题排查表

方案 原理 优点 缺点 适用场景
JTA (Java Transaction API) 使用全局事务协调器 强一致性 性能开销大,配置复杂 需要强一致性的金融系统
最终一致性 (Saga模式) 通过补偿操作实现 高性能,松耦合 实现复杂,需要补偿逻辑 高并发,可接受短暂不一致
本地消息表 通过消息队列保证 可靠性高 需要额外表存储消息 需要可靠异步处理的场景

数据源切换失败案例分析

  • 问题描述

    在动态数据源切换场景下,有时切换不生效,仍然使用默认数据源。

  • 原因分析

    1. 数据源切换代码被异常绕过,未执行
    2. 线程池场景下线程复用导致上下文污染
    3. AOP 顺序问题导致切换时机不对
  • 解决方案

    @Aspect
    @Component
    @Order(Ordered.HIGHEST_PRECEDENCE)  // 确保最先执行
    public class DataSourceAspect {
        
        @Around("@annotation(targetDataSource)")
        public Object around(ProceedingJoinPoint joinPoint, TargetDataSource targetDataSource) throws Throwable {
            String oldKey = DynamicDataSourceContextHolder.getDataSourceType();
            try {
                DynamicDataSourceContextHolder.setDataSourceType(targetDataSource.value());
                return joinPoint.proceed();
            } finally {
                // 恢复为原来的数据源
                if (oldKey != null) {
                    DynamicDataSourceContextHolder.setDataSourceType(oldKey);
                } else {
                    DynamicDataSourceContextHolder.clearDataSourceType();
                }
            }
        }
    }
    
    // 线程池配置确保清理上下文
    @Configuration
    public class ThreadPoolConfig {
        
        @Bean
        public ExecutorService asyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(5);
            executor.setMaxPoolSize(10);
            executor.setQueueCapacity(100);
            executor.setThreadNamePrefix("Async-");
            executor.setTaskDecorator(runnable -> {
                String dsKey = DynamicDataSourceContextHolder.getDataSourceType();
                return () -> {
                    try {
                        if (dsKey != null) {
                            DynamicDataSourceContextHolder.setDataSourceType(dsKey);
                        }
                        runnable.run();
                    } finally {
                        DynamicDataSourceContextHolder.clearDataSourceType();
                    }
                };
            });
            executor.initialize();
            return executor.getThreadPoolExecutor();
        }
    }
    

多数据源与缓存集成

  • 当多数据源与缓存(如 Redis)一起使用时,需要注意缓存键的设计:

    @Service
    public class CachedUserService {
        @Autowired
        private PrimaryUserRepository primaryUserRepository;
        @Autowired
        private SecondaryUserRepository secondaryUserRepository;
        @Autowired
        private RedisTemplate<String, User> redisTemplate;
        
        private String getCacheKey(String source, Long userId) {
            return String.format("user:%s:%d", source, userId);
        }
        
        @Cacheable(value = "users", key = "#root.target.getCacheKey('primary', #userId)")
        public User getPrimaryUser(Long userId) {
            return primaryUserRepository.findById(userId).orElse(null);
        }
        
        @Cacheable(value = "users", key = "#root.target.getCacheKey('secondary', #userId)")
        public User getSecondaryUser(Long userId) {
            return secondaryUserRepository.findById(userId).orElse(null);
        }
        
        @CacheEvict(value = "users", allEntries = true)
        public void clearAllUserCache() {
            // 清除所有用户缓存
        }
    }
    

总结与扩展

技术选型建议

场景 推荐方案 理由
简单多数据源,无交叉访问 独立配置多个数据源 简单直接,易于维护
需要动态切换数据源 AbstractRoutingDataSource 灵活,可运行时决定数据源
需要强一致性事务 JTA(XA) 保证ACID,但性能较低
高并发,最终一致性可接受 Saga模式 高性能,松耦合
读写分离 AOP+注解方式 透明化,对业务代码侵入小

网站公告

今日签到

点亮在社区的每一天
去签到