Spring Boot 项目中多数据源配置使用场景

在 Spring Boot 中配置多数据源是一个非常常见的需求,主要用于以下场景:

  • 读写分离:一个主数据库(Master)负责写操作,一个或多个从数据库(Slave)负责读操作,以提高性能和可用性。
  • 业务拆分:不同的业务模块使用不同的数据库(例如,用户库、订单库、商品库)。
  • 连接异构数据库:同时连接 MySQL、PostgreSQL 等不同类型的数据库。

下面我将详细介绍两种主流的实现方式:

  1. 静态方式(推荐用于业务隔离场景):通过包路径区分不同的数据源,配置简单,结构清晰。
  2. 动态方式(推荐用于读写分离场景):使用 AOP 和自定义注解,在方法级别动态切换数据源,更灵活。

方案一:静态方式(按包路径隔离)

这种方式的核心思想是为每个数据源创建一套独立的配置(DataSource, SqlSessionFactory, TransactionManager),并使用 @MapperScan 注解扫描不同包路径下的 Mapper 接口,将它们绑定到对应的数据源上。

1. 添加依赖 (pom.xml)

确保有以下依赖。通常 Spring Boot Starter 会包含大部分。

<dependencies>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.5.3.1version> 
    dependency>
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <scope>runtimescope>
    dependency>
    
    <dependency>
        <groupId>com.zaxxergroupId>
        <artifactId>HikariCPartifactId>
    dependency>
dependencies>
2. 配置文件 (application.yml)

为不同的数据源定义各自的连接信息,并用不同的前缀区分。

spring:
  datasource:
    # 主数据源配置 (master)
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db_master?serverTimezone=UTC
      username: root
      password: your_password
      type: com.zaxxer.hikari.HikariDataSource # 指定连接池类型

    # 从数据源配置 (slave)
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3307/db_slave?serverTimezone=UTC
      username: root
      password: your_password
      type: com.zaxxer.hikari.HikariDataSource
3. 创建数据源配置类

为每个数据源创建一个 Java 配置类。

主数据源配置 (MasterDataSourceConfig.java)

package com.example.config.datasource;

import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
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.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;

@Configuration
// 扫描 Master 库的 Mapper 接口
@MapperScan(basePackages = "com.example.mapper.master", sqlSessionTemplateRef = "masterSqlSessionTemplate")
public class MasterDataSourceConfig {

    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    @Primary // 标记为主数据源
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "masterSqlSessionFactory")
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // 如果有 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);
    }
}

从数据源配置 (SlaveDataSourceConfig.java)

package com.example.config.datasource;

import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
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.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;

@Configuration
// 扫描 Slave 库的 Mapper 接口
@MapperScan(basePackages = "com.example.mapper.slave", sqlSessionTemplateRef = "slaveSqlSessionTemplate")
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 {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        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);
    }
}
4. 创建 Mapper 接口

将不同数据源的 Mapper 接口放到各自的包下。

  • com.example.mapper.master -> UserMasterMapper.java
  • com.example.mapper.slave -> OrderSlaveMapper.java
5. 使用

现在你可以在 Service 中直接注入并使用对应的 Mapper,Spring 会自动为它们关联正确的数据源。

@Service
public class MyService {
    @Autowired
    private UserMasterMapper userMasterMapper; // 操作 master 库

    @Autowired
    private OrderSlaveMapper orderSlaveMapper; // 操作 slave 库

    public void doSomething() {
        // ...
        userMasterMapper.insert(someUser); // 写入主库
        Order order = orderSlaveMapper.selectById(1); // 从从库读取
    }
}

优点:配置隔离,结构非常清晰,不会混淆。
缺点:如果一个 Service 方法需要同时操作两个库,代码会稍微复杂,且默认的 @Transactional 不能跨数据源生效。


方案二:动态方式(AOP + 自定义注解)

这种方式更灵活,适用于读写分离等需要在同一个 Service 中切换数据源的场景。

1. 配置文件 (application.yml)

与方案一相同。

2. 创建自定义注解

创建一个注解,用于标记方法应该使用哪个数据源。

package com.example.config.dynamic;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "master"; // 默认使用 master 数据源
}
3. 创建数据源上下文持有者

使用 ThreadLocal 来存储当前线程需要使用的数据源 Key。

package com.example.config.dynamic;

public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }

    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}
4. 创建动态数据源类

继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey 方法,从 DataSourceContextHolder 获取当前数据源 Key。

package com.example.config.dynamic;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }
}
5. 创建AOP切面

创建一个切面,拦截 @DataSource 注解,在方法执行前设置数据源 Key,在方法执行后清除它。

package com.example.config.dynamic;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
@Order(1) // 保证该AOP在@Transactional之前执行
public class DataSourceAspect {

    @Pointcut("@annotation(com.example.config.dynamic.DataSource)")
    public void dsPointCut() {}

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);
        
        // 设置数据源
        if (dataSource != null) {
            DataSourceContextHolder.setDataSourceKey(dataSource.value());
        }

        try {
            return point.proceed();
        } finally {
            // 清除数据源,防止内存泄漏
            DataSourceContextHolder.clearDataSourceKey();
        }
    }
}
6. 整合数据源配置

创建一个统一的配置类来管理所有数据源。

package com.example.config;

import com.example.config.dynamic.DynamicDataSource;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DynamicDataSourceConfig {

    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary // 必须!将动态数据源设置为主数据源
    public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        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;
    }
}

注意:这种方式下,SqlSessionFactoryTransactionManager 只需要配置一个,并让它们使用这个 @PrimaryDynamicDataSource 即可。Spring Boot 会自动配置好。

7. 使用

在 Service 方法上添加 @DataSource 注解来切换数据源。

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    // 默认不加注解,使用 master 数据源(因为我们配置了默认值)
    @Transactional // 事务仍然有效
    public void addProduct(Product product) {
        productMapper.insert(product);
    }

    // 显式指定使用 slave 数据源
    @DataSource("slave")
    public Product getProductById(Integer id) {
        return productMapper.selectById(id);
    }
}

重要提醒:关于事务

  • 单数据源事务:在以上两种方案中,只要 DataSourceTransactionManagerSqlSessionFactory 绑定的是同一个 DataSource@Transactional 注解就能正常工作。在动态方案中,事务管理器绑定的是 DynamicDataSource,它能确保事务在当前线程选择的数据源上生效。
  • 跨数据源事务(分布式事务):标准的 @Transactional 无法管理跨多个数据源的事务。如果你需要在同一个方法中对 masterslave 都进行写操作,并保证它们的原子性,你需要引入 JTA(Java Transaction API)事务管理器,例如 AtomikosNarayana。这会增加配置的复杂度。

总结

  • 如果你的业务模块和数据库绑定关系固定,方案一(静态方式) 更简单、更直观。
  • 如果你需要实现读写分离,或者在业务逻辑中频繁切换数据源,方案二(动态方式) 提供了更高的灵活性。

你可能感兴趣的:(微服务实战系列,spring,boot,后端,java,多数据源)