SpringBoot教程(二十五) | SpringBoot配置多个数据源

时间:2024-11-13 07:31:52

SpringBoot教程(二十五) | SpringBoot配置多个数据源

  • 前言
  • 方式一:使用dynamic-datasource-spring-boot-starter
    • 引入maven依赖
    • 配置数据源
    • 动态切换数据源实战
  • 方式二:使用AbstractRoutingDataSource
    • 1. 创建数据源枚举类
    • 2. 创建数据源上下文持有者
    • 3. 创建自定义的 AbstractRoutingDataSource
    • 4. 配置yml文件
    • 5. 配置数据源
      • HikariCP 版本
      • Druid 版本
    • 6. 自定义多数据源切换注解
    • 7. 使用 AOP 或拦截器设置数据源键
    • 8. 在服务层 测试使用
    • 总结
  • 方式三:分包方式
    • 1. 添加依赖
    • 2. application.yml 配置文件
    • 3. DataSourceConfig1.java 配置类
    • 4. DataSourceConfig2.java 配置类
    • 5. Mapper接口和XML文件
    • 6. 使用Mapper接口的服务类
    • 总结

前言

SpringBoot配置多数据源指的是在一个Spring Boot项目中配置和连接到多个数据库实例的能力。
这种架构允许应用程序根据不同的业务需求、数据类型或性能要求,与多个独立的数据库环境交互。
在实现上,每个数据源都有自己的连接池、事务管理和数据访问对象。

方式一:使用dynamic-datasource-spring-boot-starter

引入maven依赖

Spring Boot 3 区别其他版本,使用的是dynamic-datasource-spring-boot3-starter ,
其他版本的 Spring Boot 使用 dynamic-datasource-spring-boot-starter , 除了依赖区别,其他配置和使用方式新老版本无差别。

Spring Boot 3

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
    <version>4.2.0</version>
</dependency>

Spring 1.5.x 及 Spring 2.x.x

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>4.2.0</version>
</dependency>

配置数据源

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为 master
      strict: false # 设置严格模式,当数据源找不到时,是否抛出异常,默认为false不抛出
      datasource:
        master: # 主库
          type: com.alibaba.druid.pool.DruidDataSource
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
          url: jdbc:mysql://www.youlai.tech:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
          username: youlai
          password: 123456
        slave: # 从库
          type: com.alibaba.druid.pool.DruidDataSource
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
          username: root
          password: 123456

动态切换数据源实战

注解切换数据源
@DS注解是 dynamic-datasource-spring-boot-starter提供的
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。

注解 结果
没有@DS 默认数据源
@DS(“dsName”) dsName可以为组名也可以为具体某个库的名称
/**
 * 主库查询
 */
@DS("master")
@Select("select * from sys_user where id = #{userId}")
SysUser getUserFromMaster(Long userId);

/**
 * 从库查询
 */
@DS("slave")
@Select("select * from sys_user where id = #{userId}")
SysUser getUserFromSlave(Long userId);

单元测试类

package com.youlai.system.mapper;

import com.youlai.system.model.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
class SysUserMapperTest {

    @Autowired
    private SysUserMapper userMapper;

    private final Long userId = 1L;

    /**
     * 测试注解方式切换数据源
     */
    @Test
    void testSwitchDataSourceByAnnotation() {
        SysUser masterUser = userMapper.getUserFromMaster(userId);
        log.info("用户ID:{} 主库姓名:{}", userId, masterUser.getNickname());
        SysUser slaveUser = userMapper.getUserFromSlave(userId);
        log.info("用户ID:{} 从库姓名:{}", userId, slaveUser.getNickname());
    }
}

测试结果

在这里插入图片描述

方式二:使用AbstractRoutingDataSource

AbstractRoutingDataSource 是 Spring 框架中用于实现多数据源路由的一个抽象类。
它允许你在运行时根据某种键(通常是线程本地变量)动态地选择数据源。

1. 创建数据源枚举类

/**
 * 数据源
 */
public enum DataSourceType {
    /**
     * 数据源1
     * */
    DB1,

    /**
     * 数据源2
     * */
    DB2
}

2. 创建数据源上下文持有者

首先,你需要一个类来持有当前线程的数据源键(DataSource Type)。这通常是通过 ThreadLocal 实现的。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 数据源切换处理
 */
public class DynamicDataSourceContextHolder {
    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType) {
        log.info("切换到{}数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

3. 创建自定义的 AbstractRoutingDataSource

接下来,你需要扩展 AbstractRoutingDataSource 并重写 determineCurrentLookupKey 方法来返回当前线程的数据源键。

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



/**
 * Spring的AbstractRoutingDataSource抽象类,实现动态数据源(他的作用就是动态切换数据源)
 * AbstractRoutingDataSource中的抽象方法determineCurrentLookupKey是实现数据源的route的核心,
 * 这里对该方法进行Override。【上下文DynamicDataSourceContextHolder为一线程安全的ThreadLocal】
 */
public class DynamicDataSource extends AbstractRoutingDataSource {


    /**
     * 取得当前使用哪个数据源
     * @return dbTypeEnum
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

4. 配置yml文件

spring:
  datasource:
    # 配置第一个数据源
    db1:
      url: jdbc:mysql://localhost:3306/db1
      username: user1
      password: pass1
      driver-class-name: com.mysql.cj.jdbc.Driver
    
    # 配置第二个数据源
    db2:
      url: jdbc:mysql://localhost:3306/db2
      username: user2
      password: pass2
      driver-class-name: com.mysql.cj.jdbc.Driver

5. 配置数据源

在你的配置类中,你需要配置多个数据源,并将它们添加到 multipleDataSource 中。

HikariCP 版本

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.example.springbootfull.quartztest.datasource.DynamicDataSource;
import com.example.springbootfull.quartztest.enums.DataSourceType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
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.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 多数据源配置
 */
@Configuration
public class DataSourceConfig {

    /**
     * 创建第一个数据源
     *
     * @return dataSource
     */
    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    public DataSource dataSource1() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 创建第二个数据源
     *
     * @return dataSource
     */
    @Bean(name = "dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.db2")
    public DataSource dataSource2() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 动态数据源配置
     * 多个相同类型的 Bean,那么就会出现歧义,需要使用@Qualifier
     * @Qualifier 是按名称来查找和注入 Bean 的
     * @return dataSource
     */
    @Primary
    @Bean("multipleDataSource")
    public DataSource multipleDataSource(@Qualifier("dataSource1") DataSource db1,
                                         @Qualifier("dataSource2") DataSource db2) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put(DataSourceType.DB1, db1);
        dataSources.put(DataSourceType.DB2, db2);
        dynamicDataSource.setTargetDataSources(dataSources);
        //默认数据源
        dynamicDataSource.setDefaultTargetDataSource(db1);
        return dynamicDataSource;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("multipleDataSource") DataSource multipleDataSource) throws Exception {
        // 导入mybatissqlsession配置
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        // 指明数据源
        sessionFactory.setDataSource(multipleDataSource);
        // 设置mapper.xml的位置路径
        Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*.xml");
        sessionFactory.setMapperLocations(resources);
        //指明实体扫描(多个package用逗号或者分号分隔)
        //sessionFactory.setTypeAliasesPackage("com.szylt.projects.project.entity");
        // 导入mybatis配置
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sessionFactory.setConfiguration(configuration);
        return sessionFactory.getObject();
    }


    //数据源事务配置
    @Bean
    public PlatformTransactionManager transactionManager(DataSource multipleDataSource) {
        return new DataSourceTransactionManager(multipleDataSource);
    }

}

Druid 版本

需要先引入 Druid 依赖,这里使用的是 Druid 官方的 Starter

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.2.8</version>
</dependency>

然后,在properties配置文件中 为每个数据源配置DruidDataSour数据库连接池

spring.datasource.db1.type=com.alibaba.druid.pool.DruidDataSour
spring.datasource.db2.type=com.alibaba.druid.pool.DruidDataSour

接着把一下DataSourceConfig 类里面的 按下面的操作,就好了

  • DataSource 对象 换成 DruidDataSource 对象
  • DataSourceBuilder 对象 换成 DruidDataSourceBuilder 对象

6. 自定义多数据源切换注解

import com.example.springbootfull.quartztest.enums.DataSourceType;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义多数据源切换注解
 * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    /**
     * 切换数据源名称
     * 默认为DB1
     */
    public DataSourceType value() default DataSourceType.DB1;
}

7. 使用 AOP 或拦截器设置数据源键

为了在使用时能够动态地切换数据源,你需要在执行数据库操作之前设置数据源键。
这通常是通过 AOP(面向切面编程)或拦截器来实现的。

需要引入aop依赖

<!--spring切面aop依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

以下是一个使用 AOP 的示例:

以下这个位置,记得 替换成你项目中 自定义注解 DataSource 所在的具体位置
@Pointcut(“@annotation(com.example.xx.xx.DataSource) || @within(com.example.xx.xx.DataSource)”)

import java.util.Objects;

import com.example.springbootfull.quartztest.annotation.DataSource;
import com.example.springbootfull.quartztest.datasource.DynamicDataSourceContextHolder;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


/**
 * 多数据源处理
 *
 */
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    //此处替换成你项目中 自定义注解 DataSource 所在的具体位置
    @Pointcut("@annotation(com.example.xx.xx.DataSource)"
            + "|| @within(com.example.xx.xx.DataSource)")
    public void dsPointCut() {

    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSource dataSource = getDataSource(point);
        if (dataSource != null) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource)) {
            return dataSource;
        }

        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

8.