Java案例如何配置多数据源?

wen java案例 77

本文目录导读:

Java案例如何配置多数据源?

  1. 核心思路
  2. 方案一:基于 Spring Boot + MyBatis 的静态多数据源
  3. 方案二:动态数据源路由 (ThreadLocal + AbstractRoutingDataSource)
  4. 方案三:使用 ShardingSphere-JDBC (推荐生产环境)
  5. 总结与建议

在Java企业级开发(尤其是Spring Boot项目中)配置多数据源是一个常见需求,用于读写分离、连接不同业务数据库或实现数据迁移。

下面我将介绍两种最主流的配置方式:静态多数据源(基于Spring Boot + JPA/MyBatis)和动态数据源路由(基于AbstractRoutingDataSource)。


核心思路

无论采用哪种方式,都离不开以下两个步骤:

  1. 定义数据源:在配置文件中声明多个DataSource Bean。
  2. 管理数据源:使用@Primary指定主数据源,并通过AbstractRoutingDataSource实现动态切换,或者在Mapper/Repository层显式指定。

基于 Spring Boot + MyBatis 的静态多数据源

这种方式最常用,通过包扫描路径或注解来区分操作哪个数据库。

配置文件 (application.yml)

spring:
  datasource:
    # 主数据源 (master)
    master:
      url: jdbc:mysql://localhost:3306/db_master?useSSL=false
      username: root
      password: root123
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 从数据源 (slave)
    slave:
      url: jdbc:mysql://localhost:3306/db_slave?useSSL=false
      username: root
      password: root123
      driver-class-name: com.mysql.cj.jdbc.Driver

数据源配置类

我们需要创建两个DataSource Bean,并指定@Primary

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 javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
    // 主数据源
    @Primary // 标记为主数据源
    @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();
    }
}

配置 MyBatis 的 SqlSessionFactory

为每个数据源创建独立的SqlSessionFactory,并为它们指定不同的包扫描路径。

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
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 javax.sql.DataSource;
@Configuration
public class MyBatisConfig {
    // 主数据源的 SqlSessionFactory
    @Bean(name = "masterSqlSessionFactory")
    @Qualifier("masterSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(
            @Qualifier("masterDataSource") DataSource masterDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(masterDataSource);
        // 指定主库的 Mapper XML 文件路径 (不同路径)
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/master/*.xml"));
        return bean.getObject();
    }
    // 从数据源的 SqlSessionFactory
    @Bean(name = "slaveSqlSessionFactory")
    public SqlSessionFactory slaveSqlSessionFactory(
            @Qualifier("slaveDataSource") DataSource slaveDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(slaveDataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/slave/*.xml"));
        return bean.getObject();
    }
}

为 Mapper 扫描指定数据源

// 主库 Mapper 扫描
@MapperScan(
    basePackages = "com.example.mapper.master",
    sqlSessionFactoryRef = "masterSqlSessionFactory"
)
public class MasterMapperConfig {
}
// 从库 Mapper 扫描
@MapperScan(
    basePackages = "com.example.mapper.slave",
    sqlSessionFactoryRef = "slaveSqlSessionFactory"
)
public class SlaveMapperConfig {
}

注意:这种方式下,你的代码结构会自然地分为mapper/mastermapper/slave两个包,调用者只需要调用对应包的 Mapper 接口,即可自动使用对应的数据源。


动态数据源路由 (ThreadLocal + AbstractRoutingDataSource)

当需要在同一次请求内根据业务逻辑动态切换数据源时(根据用户ID路由到不同的分库),方案一就不够灵活了,此时可以使用AbstractRoutingDataSource

创建数据源路由上下文

public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    // 设置当前数据源
    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }
    // 获取当前数据源
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }
    // 清除当前数据源
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}

实现 AbstractRoutingDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 返回当前线程使用的数据源名称
        return DataSourceContextHolder.getDataSource();
    }
}

配置动态数据源

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
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    @Bean
    @Primary // 必须标记为主 Bean
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 配置所有数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());
        // 设置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}

使用 AOP + 注解 实现无缝切换

为了方便,我们可以定义一个注解@DataSource,通过AOP自动设置上下文。

@DataSource 注解

import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "master"; // 默认主库
}

AOP切面实现

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) // 确保在事务之前执行
public class DataSourceAspect {
    @Pointcut("@annotation(com.example.annotation.DataSource)")
    public void dataSourcePointcut() {}
    @Around("dataSourcePointcut()")
    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) {
            dataSource = point.getTarget().getClass().getAnnotation(DataSource.class);
        }
        // 设置数据源
        if (dataSource != null) {
            DataSourceContextHolder.setDataSource(dataSource.value());
        }
        try {
            return point.proceed();
        } finally {
            // 清除数据源,避免内存泄漏
            DataSourceContextHolder.clear();
        }
    }
}

使用示例

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    @DataSource("master") // 强制走主库
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
    @DataSource("slave") // 强制走从库
    public List<User> listUsers() {
        return userMapper.selectAll();
    }
}

使用 ShardingSphere-JDBC (推荐生产环境)

如果项目是多数据源 + 分库分表 + 读写分离的复杂场景,不建议自己实现AbstractRoutingDataSource,而是直接使用成熟的第三方框架。

ShardingSphere-JDBC 配置示例:

spring:
  shardingsphere:
    datasource:
      names: master,slave
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db_master
        username: root
        password: root123
      slave:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db_slave
        username: root
        password: root123
    rules:
      readwrite-splitting:
        data-sources:
          myds:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave

这种方式完全屏蔽了底层细节,开发者只需像操作单数据源一样编写SQL,ShardingSphere会自动完成路由。


总结与建议

特性 静态多数据源 动态数据源路由 ShardingSphere
实现复杂度 低 (适合结构固定) 中 (适合简单动态切换) 高 (但功能强大)
灵活性 差 (需分包) 高 (任意方法切换) 极高
事务一致性 不支持跨库事务 不支持跨库事务 支持分布式事务 (需配置)
推荐场景 代码隔离清晰、结构固定 需要方法级别动态切换 复杂分库、读写分离、分表

我的建议:

  • 如果你的团队希望代码结构清晰、数据源之间业务独立,首选方案一
  • 如果你需要根据参数动态选择数据库(比如租户隔离),且不想引入第三方框架,使用方案二
  • 如果你项目本身就涉及到分库分表、数据加密、读写分离等复杂需求,直接上 ShardingSphere,避免重复造轮子。

抱歉,评论功能暂时关闭!