一、核心概念

在企业级应用中,一个应用可能需要访问多个不同的数据库。这些数据库可能位于不同的服务器、使用不同的数据库类型(如 MySQL 和 PostgreSQL),或者用于读写分离、业务隔离等场景。Spring Boot 通过 DataSource 配置和 @Primary 注解,可以灵活地管理多个数据源。

关键概念

  1. DataSource

    • 核心接口,代表一个数据库连接池。每个数据源都需要配置一个独立的 DataSource Bean。
    • Spring Boot 通常会自动配置一个 DataSource Bean(默认为 @Primary),但在多数据源场景下,需要手动配置所有 DataSource,并明确指定哪个是主数据源。
  2. @Primary

    • 当存在多个相同类型的 Bean 时,@Autowired 默认会报错(NoUniqueBeanDefinitionException)。@Primary 注解用于标记一个 Bean 为首选 Bean,当没有明确指定注入哪个时,优先注入被 @Primary 标记的 Bean。
    • 在多数据源配置中,必须为一个 DataSource 添加 @Primary 注解,以解决自动装配的歧义。
  3. JdbcTemplate / NamedParameterJdbcTemplate

    • 这些模板类依赖于 DataSource。在多数据源环境下,需要为每个 DataSource 创建对应的 JdbcTemplate Bean。
    • 通过构造函数注入特定的 DataSource 来创建。
  4. EntityManagerFactory / TransactionManager (JPA/Hibernate):

    • 如果使用 JPA,每个数据源通常需要独立的 LocalContainerEntityManagerFactoryBean (负责实体管理) 和 PlatformTransactionManager (负责事务管理)。
    • 需要通过 @Qualifier 注解明确指定使用哪个 DataSourceTransactionManager
  5. @Qualifier

    • 当存在多个同类型 Bean 且没有 @Primary,或者需要注入非主 Bean 时,使用 @Qualifier 注解通过 Bean 名称来精确指定注入哪一个。
    • Bean 名称通常是方法名(在 @Bean 方法上)或类名(在 @Component 类上)。
  6. 配置隔离:

    • 通常将不同数据源的配置(DataSource, JdbcTemplate, EntityManagerFactory, TransactionManager)放在独立的配置类中,以保持代码清晰和可维护性。
  7. 读写分离:

    • 一种常见的多数据源应用模式。配置一个主库(Master,用于写操作)和一个或多个从库(Slave/Read Replica,用于读操作)。
    • 可以通过 AOP 切面、自定义 AbstractRoutingDataSource 或使用 ShardingSphere 等中间件实现自动路由。

二、操作步骤(非常详细)

场景设定

配置两个数据源:

  • 主数据源 (primary): MySQL 数据库 db_primary,用于核心业务。
  • 次数据源 (secondary): MySQL 数据库 db_secondary,用于日志或报表。

步骤 1:添加依赖

pom.xml:

<dependencies>
    <!-- Spring Boot Web Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot JDBC Starter (如果使用 JdbcTemplate) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <!-- Spring Boot Data JPA Starter (如果使用 JPA) -->
    <!-- <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency> -->

    <!-- MySQL Driver -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- (可选) Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
  • 注意: 根据实际需求选择使用 spring-boot-starter-jdbc (JdbcTemplate) 或 spring-boot-starter-data-jpa (JPA/Hibernate)。

步骤 2:配置文件 (application.yml)

# application.yml
spring:
  # 主数据源配置 (primary)
  datasource:
    primary:
      url: jdbc:mysql://localhost:3306/db_primary?useSSL=false&serverTimezone=UTC
      username: primary_user
      password: primary_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        pool-name: PrimaryHikariCP
        maximum-pool-size: 15
    # 次数据源配置 (secondary)
    secondary:
      url: jdbc:mysql://localhost:3306/db_secondary?useSSL=false&serverTimezone=UTC
      username: secondary_user
      password: secondary_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        pool-name: SecondaryHikariCP
        maximum-pool-size: 10

  # (可选) JPA 配置 (如果使用 JPA)
  # jpa:
  #   show-sql: true
  #   hibernate:
  #     ddl-auto: update
  #   properties:
  #     hibernate:
  #       format_sql: true
  • 关键点: 将数据源配置放在 spring.datasource.primaryspring.datasource.secondary 下,避免与 Spring Boot 默认的 spring.datasource 冲突。主数据源的 @Primary 注解将在配置类中指定。

步骤 3:创建数据库和表

  1. 创建数据库:

    CREATE DATABASE IF NOT EXISTS db_primary;
    CREATE DATABASE IF NOT EXISTS db_secondary;
    
  2. db_primary 中创建表:

    USE db_primary;
    CREATE TABLE IF NOT EXISTS users (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL
    );
    
  3. db_secondary 中创建表:

    USE db_secondary;
    CREATE TABLE IF NOT EXISTS logs (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        message TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    

步骤 4:创建实体类 (如果使用 JPA)

  • User.java (主数据源):

    package com.example.multidatasource.entity.primary;
    
    import javax.persistence.*;
    
    @Entity
    @Table(name = "users")
    @Data // Lombok
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private String email;
    }
    
    • 注意包路径: com.example.multidatasource.entity.primary
  • Log.java (次数据源):

    package com.example.multidatasource.entity.secondary;
    
    import javax.persistence.*;
    
    @Entity
    @Table(name = "logs")
    @Data
    public class Log {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String message;
        @Column(name = "created_at")
        private java.sql.Timestamp createdAt;
    }
    
    • 注意包路径: com.example.multidatasource.entity.secondary

步骤 5:配置主数据源 (Primary)

创建配置类 PrimaryDataSourceConfig.java

package com.example.multidatasource.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class PrimaryDataSourceConfig {

    /**
     * 创建主数据源 Bean
     * @return 配置好的 DataSource
     */
    @Primary // 标记为主数据源
    @Bean(name = "primaryDataSource") // 指定 Bean 名称
    @ConfigurationProperties("spring.datasource.primary") // 绑定配置前缀
    public DataSource primaryDataSource() {
        // Spring Boot 会自动根据类型选择合适的 DataSource (如 HikariDataSource)
        return DataSourceBuilder.create().build();
    }

    /**
     * 为主数据源创建 JdbcTemplate Bean
     * @param dataSource 通过 @Qualifier 注入名为 "primaryDataSource" 的 Bean
     * @return 配置好的 JdbcTemplate
     */
    @Bean(name = "primaryJdbcTemplate")
    public JdbcTemplate primaryJdbcTemplate(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    // 如果使用 JPA, 需要在此配置 LocalContainerEntityManagerFactoryBean 和 PlatformTransactionManager
    // 但为了清晰,通常将 JPA 配置放在单独的类中 (见后续步骤)
}

步骤 6:配置次数据源 (Secondary)

创建配置类 SecondaryDataSourceConfig.java

package com.example.multidatasource.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class SecondaryDataSourceConfig {

    /**
     * 创建次数据源 Bean
     * @return 配置好的 DataSource
     */
    @Bean(name = "secondaryDataSource") // 注意:没有 @Primary
    @ConfigurationProperties("spring.datasource.secondary") // 绑定配置前缀
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 为次数据源创建 JdbcTemplate Bean
     * @param dataSource 通过 @Qualifier 注入名为 "secondaryDataSource" 的 Bean
     * @return 配置好的 JdbcTemplate
     */
    @Bean(name = "secondaryJdbcTemplate")
    public JdbcTemplate secondaryJdbcTemplate(
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

步骤 7:配置 JPA (如果使用 JPA)

如果使用 JPA,需要为每个数据源配置独立的 EntityManagerFactoryTransactionManager跳过此步骤如果只使用 JdbcTemplate

  1. 主数据源 JPA 配置 (PrimaryJpaConfig.java):

    package com.example.multidatasource.config.jpa;
    
    import com.example.multidatasource.entity.primary.User;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.orm.jpa.JpaTransactionManager;
    import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.persistence.EntityManagerFactory;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    @Configuration
    @EnableTransactionManagement // 启用注解式事务管理
    @EnableJpaRepositories(
        basePackages = "com.example.multidatasource.repository.primary", // 指定该 EntityManagerFactory 管理的 Repository 包
        entityManagerFactoryRef = "primaryEntityManagerFactory", // 指定 EntityManagerFactory Bean 名称
        transactionManagerRef = "primaryTransactionManager" // 指定 TransactionManager Bean 名称
    )
    public class PrimaryJpaConfig {
    
        /**
         * 创建主数据源的 EntityManagerFactory Bean
         * @param builder EntityManagerFactoryBuilder
         * @param dataSource 通过 @Qualifier 注入主数据源
         * @return LocalContainerEntityManagerFactoryBean
         */
        @Primary
        @Bean(name = "primaryEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
                EntityManagerFactoryBuilder builder,
                @Qualifier("primaryDataSource") DataSource dataSource) {
    
            Map<String, Object> properties = new HashMap<>();
            properties.put("hibernate.hbm2ddl.auto", "update"); // 根据需要调整
            properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
            properties.put("hibernate.show_sql", "true");
    
            return builder
                    .dataSource(dataSource)
                    .packages(User.class) // 指定实体类包或具体类
                    .persistenceUnit("primary") // 持久化单元名称
                    .properties(properties)
                    .build();
        }
    
        /**
         * 创建主数据源的事务管理器 Bean
         * @param emf 通过 @Qualifier 注入主 EntityManagerFactory
         * @return PlatformTransactionManager
         */
        @Primary
        @Bean(name = "primaryTransactionManager")
        public PlatformTransactionManager primaryTransactionManager(
                @Qualifier("primaryEntityManagerFactory") EntityManagerFactory emf) {
            return new JpaTransactionManager(emf);
        }
    }
    
  2. 次数据源 JPA 配置 (SecondaryJpaConfig.java):

    package com.example.multidatasource.config.jpa;
    
    import com.example.multidatasource.entity.secondary.Log;
    import org.springframework.beans.factory.annotation.Qualifier;
    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.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.persistence.EntityManagerFactory;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    @Configuration
    @EnableTransactionManagement
    @EnableJpaRepositories(
        basePackages = "com.example.multidatasource.repository.secondary", // 指定 Repository 包
        entityManagerFactoryRef = "secondaryEntityManagerFactory", // 指定 EMF Bean 名称
        transactionManagerRef = "secondaryTransactionManager" // 指定 TM Bean 名称
    )
    public class SecondaryJpaConfig {
    
        /**
         * 创建次数据源的 EntityManagerFactory Bean
         * @param builder
         * @param dataSource 通过 @Qualifier 注入次数据源
         * @return
         */
        @Bean(name = "secondaryEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
                EntityManagerFactoryBuilder builder,
                @Qualifier("secondaryDataSource") DataSource dataSource) {
    
            Map<String, Object> properties = new HashMap<>();
            properties.put("hibernate.hbm2ddl.auto", "update");
            properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
            properties.put("hibernate.show_sql", "true");
    
            return builder
                    .dataSource(dataSource)
                    .packages(Log.class)
                    .persistenceUnit("secondary")
                    .properties(properties)
                    .build();
        }
    
        /**
         * 创建次数据源的事务管理器 Bean
         * @param emf 通过 @Qualifier 注入次 EntityManagerFactory
         * @return
         */
        @Bean(name = "secondaryTransactionManager")
        public PlatformTransactionManager secondaryTransactionManager(
                @Qualifier("secondaryEntityManagerFactory") EntityManagerFactory emf) {
            return new JpaTransactionManager(emf);
        }
    }
    

步骤 8:创建 Repository (DAO)

方式 A:使用 JdbcTemplate

  1. 主数据源 Repository (PrimaryUserRepository.java):

    package com.example.multidatasource.repository;
    
    import com.example.multidatasource.entity.primary.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    import java.util.Optional;
    
    @Repository
    public class PrimaryUserRepository {
    
        private final JdbcTemplate jdbcTemplate;
    
        // 构造函数注入,使用 @Qualifier 指定注入哪个 JdbcTemplate Bean
        @Autowired
        public PrimaryUserRepository(@Qualifier("primaryJdbcTemplate") JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    
        public List<User> findAll() {
            String sql = "SELECT id, name, email FROM users";
            return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
        }
    
        public Optional<User> findById(Long id) {
            String sql = "SELECT id, name, email FROM users WHERE id = ?";
            try {
                User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), id);
                return Optional.ofNullable(user);
            } catch (Exception e) {
                return Optional.empty();
            }
        }
    
        public void save(User user) {
            String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
            jdbcTemplate.update(sql, user.getName(), user.getEmail());
        }
    }
    
  2. 次数据源 Repository (SecondaryLogRepository.java):

    package com.example.multidatasource.repository;
    
    import com.example.multidatasource.entity.secondary.Log;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    @Repository
    public class SecondaryLogRepository {
    
        private final JdbcTemplate jdbcTemplate;
    
        @Autowired
        public SecondaryLogRepository(@Qualifier("secondaryJdbcTemplate") JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    
        public List<Log> findAll() {
            String sql = "SELECT id, message, created_at FROM logs";
            return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Log.class));
        }
    
        public void save(Log log) {
            String sql = "INSERT INTO logs (message) VALUES (?)";
            jdbcTemplate.update(sql, log.getMessage());
        }
    }
    

方式 B:使用 JPA (如果配置了 JPA)

  1. 主数据源 Repository 接口 (PrimaryUserRepository.java):

    package com.example.multidatasource.repository.primary;
    
    import com.example.multidatasource.entity.primary.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface PrimaryUserRepository extends JpaRepository<User, Long> {
        // Spring Data JPA 自动生成实现
    }
    
    • 注意包路径: com.example.multidatasource.repository.primary
  2. 次数据源 Repository 接口 (SecondaryLogRepository.java):

    package com.example.multidatasource.repository.secondary;
    
    import com.example.multidatasource.entity.secondary.Log;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface SecondaryLogRepository extends JpaRepository<Log, Long> {
    }
    
    • 注意包路径: com.example.multidatasource.repository.secondary

步骤 9:创建 Service 层

package com.example.multidatasource.service;

import com.example.multidatasource.entity.primary.User;
import com.example.multidatasource.entity.secondary.Log;
import com.example.multidatasource.repository.PrimaryUserRepository; // 使用 JdbcTemplate 版本
import com.example.multidatasource.repository.SecondaryLogRepository; // 使用 JdbcTemplate 版本
// import com.example.multidatasource.repository.primary.PrimaryUserRepository; // 使用 JPA 版本
// import com.example.multidatasource.repository.secondary.SecondaryLogRepository; // 使用 JPA 版本
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class DataService {

    private final PrimaryUserRepository primaryUserRepository;
    private final SecondaryLogRepository secondaryLogRepository;

    @Autowired
    public DataService(PrimaryUserRepository primaryUserRepository,
                       SecondaryLogRepository secondaryLogRepository) {
        this.primaryUserRepository = primaryUserRepository;
        this.secondaryLogRepository = secondaryLogRepository;
    }

    // 读取主数据源
    public List<User> getAllUsers() {
        return primaryUserRepository.findAll();
    }

    // 写入主数据源
    @Transactional(transactionManager = "primaryTransactionManager") // 如果使用 JPA, 指定 TM
    public void createUser(User user) {
        primaryUserRepository.save(user);
        // 可以在这里记录操作日志到次数据源
        logOperation("Created user: " + user.getName());
    }

    // 读取次数据源
    public List<Log> getAllLogs() {
        return secondaryLogRepository.findAll();
    }

    // 写入次数据源
    @Transactional(transactionManager = "secondaryTransactionManager") // 如果使用 JPA, 指定 TM
    private void logOperation(String message) {
        Log log = new Log();
        log.setMessage(message);
        secondaryLogRepository.save(log);
    }
}

步骤 10:创建 Controller 层

package com.example.multidatasource.controller;

import com.example.multidatasource.entity.primary.User;
import com.example.multidatasource.entity.secondary.Log;
import com.example.multidatasource.service.DataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api")
public class DataController {

    private final DataService dataService;

    @Autowired
    public DataController(DataService dataService) {
        this.dataService = dataService;
    }

    @GetMapping("/users")
    public ResponseEntity<List<User>> getAllUsers() {
        return ResponseEntity.ok(dataService.getAllUsers());
    }

    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody User user) {
        dataService.createUser(user);
        return ResponseEntity.ok(user);
    }

    @GetMapping("/logs")
    public ResponseEntity<List<Log>> getAllLogs() {
        return ResponseEntity.ok(dataService.getAllLogs());
    }
}

步骤 11:启动应用并测试

  1. 确保两个数据库 (db_primary, db_secondary) 和对应的表 (users, logs) 已创建。
  2. 运行 Spring Boot 应用。
  3. 测试 API:
    • GET http://localhost:8080/api/users -> 应返回 db_primary.users 表的数据。
    • POST http://localhost:8080/api/users (JSON Body) -> 应在 db_primary.users 插入数据,并在 db_secondary.logs 记录日志。
    • GET http://localhost:8080/api/logs -> 应返回 db_secondary.logs 表的数据。

三、常见错误

  1. No qualifying bean of type 'javax.sql.DataSource' availableNoUniqueBeanDefinitionException:

    • 原因: Spring Boot 试图自动配置 DataSource,但在多数据源场景下,它不知道该配置哪个,或者没有明确的 @Primary 数据源。
    • 解决: 确保手动配置所有 DataSource Bean,并且为其中一个 DataSource Bean 添加 @Primary 注解
  2. UnsatisfiedDependencyException 注入 JdbcTemplate/EntityManagerFactory/TransactionManager 失败:

    • 原因: 存在多个同类型的 Bean,Spring 不知道注入哪一个。
    • 解决: 使用 @Qualifier("beanName") 注解明确指定要注入的 Bean 名称。检查 @Bean 方法的名称或 @Component 类的名称。
  3. InvalidDataAccessApiUsageException / PersistenceException: No EntityManager with actual transaction available for current thread:

    • 原因 (JPA): 在需要事务的方法上缺少 @Transactional 注解,或者 @Transactional 注解没有正确指定 transactionManager
    • 解决 (JPA): 对于写操作(增删改),确保 Service 方法上有 @Transactional。如果使用多事务管理器,使用 @Transactional(transactionManager = "yourTransactionManagerBeanName") 指定。
  4. BeanCreationException 创建 EntityManagerFactory 失败:

    • 原因: @EnableJpaRepositoriesbasePackages 指向了错误的包,或者实体类 (@Entity) 没有被扫描到。
    • 解决: 检查 @EnableJpaRepositories(basePackages = ...) 的包路径是否正确包含了对应的 Repository 接口和实体类。确保实体类在正确的包下。
  5. SQLSyntaxErrorException / TableNotFoundException:

    • 原因: SQL 语句中的表名或列名错误,或者数据库连接指向了错误的数据库。
    • 解决: 仔细检查 SQL 语句;确认 DataSource 的 URL 配置正确,连接到了预期的数据库。
  6. DataSource 配置未生效 (@ConfigurationProperties 不工作):

    • 原因: @ConfigurationProperties 的前缀与 application.yml 中的配置路径不匹配。
    • 解决: 仔细核对 @ConfigurationProperties("prefix") 中的 prefix 是否与 application.yml 中的配置节点完全一致(包括拼写和层级)。
  7. JdbcTemplate Bean 注入了错误的数据源:

    • 原因: 创建 JdbcTemplate Bean 时,注入的 DataSource 参数没有使用 @Qualifier,导致注入了 @Primary 的数据源。
    • 解决: 在创建非主 JdbcTemplate@Bean 方法中,为其 DataSource 参数添加 @Qualifier("dataSourceBeanName")

四、注意事项

  1. 禁用自动配置: 在多数据源配置中,不要依赖 Spring Boot 对 DataSource 的自动配置。必须手动创建所有 DataSource Bean。
  2. @Primary 的必要性: 必须且只能有一个 DataSource Bean 被标记为 @Primary,以解决自动装配的歧义。
  3. @Qualifier 的使用: 当需要注入非主 Bean 时,@Qualifier 是必不可少的。
  4. Bean 名称: @Bean 方法的名称默认就是 Bean 的名称。确保 @Qualifier 中引用的名称与 @Bean 方法名一致。
  5. JPA 配置复杂性: 使用 JPA 时,配置 EntityManagerFactoryTransactionManager 会显著增加复杂度。确保 @EnableJpaRepositories 的配置正确。
  6. 事务边界: 在多数据源环境下,默认的 @Transactional 只能管理一个数据源的事务。跨数据源的分布式事务需要使用 JTA (如 Atomikos, Bitronix) 或最终一致性方案(如 Saga 模式),这非常复杂且性能开销大,应尽量避免。
  7. 连接池监控: 为每个数据源配置独立的连接池监控(如 HikariCP 的 metrics),以便观察各自的数据源健康状况。
  8. 配置隔离: 将不同数据源的配置分散到不同的配置类中,保持代码清晰。
  9. 测试: 多数据源应用的集成测试需要更谨慎,确保测试数据在正确的数据库中。

五、使用技巧

  1. 使用常量类定义 Bean 名称: 避免在代码中硬编码 Bean 名称,减少拼写错误。

    public class DataSourceConstants {
        public static final String PRIMARY_DATASOURCE = "primaryDataSource";
        public static final String SECONDARY_DATASOURCE = "secondaryDataSource";
        public static final String PRIMARY_JDBC_TEMPLATE = "primaryJdbcTemplate";
        // ...
    }
    // 使用: @Qualifier(DataSourceConstants.PRIMARY_DATASOURCE)
    
  2. 抽象配置父类 (可选): 如果多个数据源配置相似,可以创建一个父类或使用 @ConfigurationProperties 的嵌套对象来减少重复代码。

  3. 动态数据源 (AbstractRoutingDataSource): 实现更复杂的路由逻辑,如读写分离。

    • 创建一个 RoutingDataSource 继承 AbstractRoutingDataSource
    • 重写 determineCurrentLookupKey() 方法,根据 ThreadLocal 变量(如 DataSourceContextHolder)决定使用哪个数据源。
    • 在 AOP 切面或方法调用前设置数据源键。
    • 注意: 这增加了复杂性,需谨慎使用。
  4. 使用 @ConfigurationProperties 这是绑定外部配置到 Java 对象的标准方式,比手动 @Value 注解更清晰、更安全。

  5. DataSource 指定唯一名称:HikariDataSource 中设置 poolName,便于在日志和监控中区分。

  6. JdbcTemplate 命名: 为每个 JdbcTemplate Bean 起一个有意义的名称(如 primaryJdbcTemplate),方便识别。


六、最佳实践

  1. 明确主次: 清晰定义哪个是主数据源(@Primary),哪个是次数据源。
  2. 配置类分离: 将每个数据源的 DataSource, JdbcTemplate, (JPA 的) EntityManagerFactory, TransactionManager 配置放在独立的 @Configuration 类中。
  3. 包结构清晰: 为不同数据源的实体类、Repository、Service 建立独立的包结构(如 entity.primary, repository.secondary)。
  4. Repository 层隔离: 确保 Repository 只访问其对应的数据源。
  5. Service 层协调: Service 层负责协调多个 Repository 的调用。
  6. 避免跨数据源事务: 除非绝对必要,否则避免在单个事务中操作多个数据源。采用补偿事务、消息队列等最终一致性方案。
  7. 使用连接池: 务必使用 HikariCP 等高性能连接池。
  8. 监控: 集成 Micrometer 等监控框架,暴露各数据源连接池的指标。
  9. 文档化: 记录数据源的用途、配置和访问方式。
  10. 安全性: 确保数据库凭证安全存储(如使用 Spring Cloud Config Server, HashiCorp Vault, 或环境变量)。

七、性能优化

  1. 连接池优化:

    • HikariCP: 精细调整 maximum-pool-size, minimum-idle, connection-timeout, idle-timeout, max-lifetime。启用预处理语句缓存 (cachePrepStmts, prepStmtCacheSize, prepStmtCacheSqlLimit, useServerPrepStmts)。
    • 监控: 实时监控连接池的活跃连接、空闲连接、等待线程数,及时发现瓶颈。
  2. SQL 优化:

    • 索引: 为频繁查询的字段建立索引。
    • 查询优化: 避免 SELECT *,使用分页,优化 JOINWHERE 条件。
    • 批量操作: 对大量数据使用 JdbcTemplate.batchUpdate
  3. 缓存:

    • 应用层缓存: 使用 @Cacheable 缓存频繁读取且不常变的数据(如配置信息、字典表),减少数据库访问。
    • 数据库缓存: 利用数据库自身的查询缓存(如果适用)。
  4. 读写分离:

    • 将读操作路由到从库,写操作路由到主库,分散主库压力。
    • 可通过 AbstractRoutingDataSource + AOP 实现,或使用 ShardingSphere 等专业中间件。
  5. 异步处理:

    • 对于非关键路径的数据库操作(如记录日志、发送通知),使用 @Async 在后台线程执行,提高主流程响应速度。
  6. 减少网络开销:

    • 确保应用服务器与数据库服务器之间的网络延迟尽可能低。
    • 使用连接池减少连接建立的开销。
  7. 监控与分析:

    • 使用 APM 工具(如 SkyWalking, Zipkin)追踪 SQL 执行时间。
    • 开启数据库慢查询日志,分析并优化慢 SQL。
    • 监控各数据源的 QPS、响应时间、错误率。

总结: Spring Boot 多数据源配置需要手动管理 DataSourceJdbcTemplate/EntityManagerFactoryTransactionManager Bean。核心是使用 @Primary 指定主数据源,并用 @Qualifier 精确注入非主 Bean。配置应清晰分离,遵循分层架构。务必注意事务管理的局限性(单数据源事务)。通过合理的连接池配置、SQL 优化、缓存和读写分离等手段,可以有效提升多数据源应用的性能。