目 录CONTENT

文章目录

SpringBoot多数据源集成——MySQL

陌念
2024-08-31 / 0 评论 / 3 点赞 / 19 阅读 / 0 字
温馨提示:
本文最后更新于2024-12-02,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

在Spring Boot项目中实现根据条件动态切换到不同的MySQL数据库进行查询,可以通过配置多数据源来实现。需要为每个数据库配置一个数据源,然后根据业务逻辑动态选择数据源进行操作。基于条件动态切换不同的MySQL数据库,可以使用AbstractRoutingDataSource来配置多数据源,并结合ThreadLocal来存储当前线程所需的数据源标识。

以下贴出的代码,都是最终版的代码,注意事项都在后面有说明。

一、Maven依赖

首先,在你的pom.xml中确保包含以下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.6</version>
    </dependency>
</dependencies>

二、配置application.yml

application.yml中配置多个数据源。

spring:
  datasource:
    dynamic:
	  # 指定默认使用default数据源配置
      primary: default
      datasource:
        default:
          url: jdbc:mysql://127.0.0.1:3306/dmp?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.alibaba.druid.pool.DruidDataSource
          druid:
            initial-size: "5"
            min-idle: "5"
            max-active: "20"
            max-wait: "60000"
            time-between-eviction-runs-millis: "60000"
            min-evictable-idle-time-millis: "300000"
            validation-query: SELECT 1 FROM DUAL
            test-while-idle: "true"
            test-on-borrow: "false"
            test-on-return: "false"
            pool-prepared-statements: "true"
            max-pool-prepared-statement-per-connection-size: "20"
            filters: stat,wall
            connectionProperties: druid.stat.mergeSql
            removeAbandoned: "true"
            removeAbandonedTimeout: "1800"
        csfa:
          url: jdbc:mysql://127.0.0.1:3306/dmp_csfa?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.alibaba.druid.pool.DruidDataSource
          druid: ${spring.datasource.dynamic.datasource.default.druid}

如果想要共用链接池配置,可以用配置文件里面的引用变量方式${spring.datasource.dynamic.datasource.default.druid}

三、配置多数据源

DataSourceConfig类中配置多个数据源。

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.rongxin.dmp.mdm.collect.enums.BusinessTabEnum;

@Configuration
public class DataSourceConfig {

  @Bean(name = "defaultDataSource")
  // 根据前缀去配置文件中找到对应的配置
  @ConfigurationProperties("spring.datasource.dynamic.datasource.default")
  public DataSource defaultDataSource() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean("csfaDataSource")
  @ConfigurationProperties("spring.datasource.dynamic.datasource.csfa")
  public DataSource csfaDataSource() {
    return DruidDataSourceBuilder.create().build();
  }

  // 添加这个注解,标记为默认数据源
  @Primary
  @Bean(name = "dynamicDataSource")
  public DynamicDataSource dynamicDataSource(
	  // 延迟加载
      @Lazy @Qualifier("defaultDataSource") DataSource defaultDataSource,
      @Lazy @Qualifier("csfaDataSource") DataSource csfaDataSource) {
    DynamicDataSource dynamicDataSource = new DynamicDataSource();
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put("cac", defaultDataSource);
    targetDataSources.put("csfa", csfaDataSource);
    // 将多数据源设置进去
    dynamicDataSource.setTargetDataSources(targetDataSources);
    // 默认数据源
    dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
    return dynamicDataSource;
  }

  @Bean
  public PlatformTransactionManager transactionManager(
      @Qualifier("dynamicDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
  }
}

注意:

  1. 如果上面没有标注@Lazy注解的话,启动可能会报错:Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'dynamicDataSource': Requested bean is currently in creation: Is there an unresolvable circular reference?

  • 原因:BeanCurrentlyInCreationException 表示在创建dynamicDataSource Bean时发生了循环依赖。通常,这是由于Spring在初始化dynamicDataSource时,需要依赖其他数据源Bean(例如default和csfa),而这些数据源可能也在依赖dynamicDataSource。

  • 解决:在注入时使用@Lazy注解(推荐),即可避免。或者通过ObjectProvider来延迟获取数据源,避免在Bean初始化期间立即创建依赖的Bean。

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dynamicDataSource(ObjectProvider<DataSource> defaultProvider,
                                               ObjectProvider<DataSource> csfaProvider) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("cac", defaultProvider.getIfAvailable());
        targetDataSources.put("csfa", csfaProvider.getIfAvailable());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(defaultProvider.getIfAvailable());
        return dynamicDataSource;
    }
  1. 如果上面没有标注@Primary注解的话,启动可能会报错:Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected single matching bean but found 3: defaultDataSource,csfaDataSource,dynamicDataSource

    • 原因:这个错误是因为在Spring容器中存在多个DataSource bean,而在某些地方(例如事务管理器或MyBatis)Spring无法确定应该使用哪个DataSource,导致抛出了NoUniqueBeanDefinitionException

    • 解决:在使用@Autowired注入 DataSource 时明确指定要使用的 DataSource bean(如上述注入事务管理器时使用的@Qualifier("dynamicDataSource"))。或者你也可以通过设置@Primary注解来指定默认的 DataSource。

四、配置数据源切换

创建一个DynamicDataSource类,通过继承AbstractRoutingDataSource实现动态数据源切换。

import org.apache.commons.lang3.StringUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import com.rongxin.dmp.mdm.collect.enums.BusinessTabEnum;

public class DynamicDataSource extends AbstractRoutingDataSource {

  private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

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

  public static void clearDataSourceKey() {
    CONTEXT_HOLDER.remove();
  }

  public static String getDataSourceKey() {
    String key = CONTEXT_HOLDER.get();
	// 如果等于空,则使用默认数据源key
    return StringUtils.isNotBlank(key) ? key : "cac";
  }

  @Override
  protected Object determineCurrentLookupKey() {
    return getDataSourceKey();
  }
}

五、动态切换数据源

有三种可选的切换数据源方法

1. 手动切换

在需要切换数据源的地方,使用DynamicDataSource.setDataSourceKey("dbKey")来指定使用哪个数据源。

import com.example.config.DynamicDataSource;
import com.example.mapper.MyMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MyService {

    private final MyMapper myMapper;

    public MyService(MyMapper myMapper) {
        this.myMapper = myMapper;
    }

    @Transactional

    public void performDbOperations(String dbKey) {
        try {
            // 切换数据源
            DynamicDataSource.setDataSourceKey(dbKey);
            // 执行数据库操作
            myMapper.someDbOperation();
        } finally {
            DynamicDataSource.clearDataSourceKey();
        }
    }
}

这样,基于传入的dbKey参数,可以在不同的数据库之间动态切换执行操作。确保在操作完之后,调用DynamicDataSource.clearDataSourceKey()以防止线程污染。

2. 使用注解自动切换

a. 定义自定义注解

首先,定义一个注解用于指定方法使用的数据源。

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

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DSSwitch {
  String value() default "cac";
}

该注解可以作用在类上,也可作用在方法上,默认值是cac

使用的时候不指定值,@DSSwitch 则使用默认的cac ,使用指定的值@DSSwitch("csfa")

b. 创建AOP切面

创建一个AOP切面拦截带有@DataSource注解的方法,并在方法执行前设置数据源。

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

  @Pointcut("@within(com.rongxin.dmp.mdm.common.anno.DSSwitch) || @annotation(com.rongxin.dmp.mdm.common.anno.DSSwitch)")
  public void dataSourcePointCut() {}

  @Before("dataSourcePointCut() && @annotation(ds)")
  public void before(DSSwitch ds) {
    DynamicDataSource.setDataSourceKey(ds.value());
  }

  @Before("dataSourcePointCut() && @within(ds)")
  public void beforeClass(DSSwitch ds) {
    DynamicDataSource.setDataSourceKey(ds.value());
  }

  // @Around("dataSourcePointCut()")
  // public Object around(ProceedingJoinPoint point) throws Throwable {
  //   try {
  //     // 执行方法
  //     return point.proceed();
  //   } finally {
  //     // 清理上下文
  //     DynamicDataSource.clearDataSourceKey();
  //   }
  // }

  @After("dataSourcePointCut()")
  public void afterSwitchDataSource() {
    DynamicDataSource.clearDataSourceKey();
  }
}

切入点@Pointcut 说明:

  • @within(com.rongxin.dmp.mdm.common.anno.DSSwitch):这个条件匹配任何被 com.rongxin.dmp.mdm.common.anno.DSSwitch 注解的类。也就是说,如果一个类被标注了 @DSSwitch 注解,那么切点将会匹配这个类中的所有方法。

  • @annotation(com.rongxin.dmp.mdm.common.anno.DSSwitch):这个条件匹配任何被 com.rongxin.dmp.mdm.common.anno.DSSwitch 注解的单个方法。也就是说,如果一个方法被标注了 @DSSwitch 注解,那么切点将会匹配这个方法。

@Around@After 的区别:

  • @Around 可以在方法执行之前和执行之后分别进行其他逻辑操作(日志记录、性能监控等)。

  • @After 只能在方法执行完之后进行逻辑操作,只能进行有限的操作。

c. 使用注解指定数据源

在需要指定数据源的方法或类上,使用@DataSource注解。

import com.example.annotation.DataSource;
import com.example.mapper.MyMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

// 指定使用 default 数据源
@DataSource("default")
@Service
public class MyService {

    private final MyMapper myMapper;

    public MyService(MyMapper myMapper) {
        this.myMapper = myMapper;
    }

    // 指定使用 csfa 数据源
    @DataSource("csfa")
    @Transactional
    public void performDbOperations() {
        // 执行数据库操作
        myMapper.someDbOperation();
    }
}

上面标注的意思是,MyService 整个类所有的方法都使用default 数据源,而方法performDbOperations() 则使用csfa 数据源。

注意

如果AOP切面没有生效,以下是可能的解决方案:

  1. 没有启用AOP代理,在启动类加上注解可能会解决@EnableAspectJAutoProxy

  2. 可能存在其他AOP代理或者拦截器在数据源切换时重新设置了数据源。

  3. 如果你的方法或者类被@Transactional注解修饰,Spring的事务管理可能会在方法执行前就已经确定了数据源。由于事务是在拦截器或AOP代理层开始的,如果在方法执行时再切换数据源,事务上下文已经绑定在之前的数据源上,所以切换无效。

    • 可以在事务开始前确定数据源,或者对于希望使用指定数据源的方法新启动一个事务(REQUIRES_NEW)。

    • 设置注解AOP切面的执行顺序:在切面类上添加注解@Order(-1) 即可。

3. 使用拦截器自动切换

a. 添加拦截器

使用 Spring 的拦截器来拦截每个请求,从请求头中提取 bus-tab 并设置到DynamicDataSource 中。

@Component
public class DataSourceInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String busTab = request.getHeader("bus-tab");

        // 根据 bus-tab 设置数据源
        DynamicDataSource.setDataSourceKey(busTab);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清理数据源标识
        DynamicDataSource.clearDataSourceKey();
    }
}

b. 配置拦截器

确保拦截器在 Spring 的配置中生效。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private DataSourceInterceptor dataSourceInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(dataSourceInterceptor);
    }
}

配置好拦截器之后,每次发送的请求中,请求头都会携带一个bus-tab 标识,根据标识可以动态切换数据源,不需要再去手动设置了,也不需要在类或方法上添加注解去动态切换。

当然,也可以把上面的三种切换方式组合使用,达到更加精细化的配置。

除了上面的配置动态多数据源切换,还能使用Apache ShardingSphere 实现动态数据源的切换,有兴趣的可以去了解下。

3
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
  3. QQ打赏

    qrcode qq

评论区