Bootstrap

springboot 配置多数据源以及动态切换数据源

场景

我们springboot项目,通常会有多个数据库,例如mysql,vertica,postgresql等等数据库,通常我们需要动态切换使用我们想要的数据库,这时候就需要配置多数据源了

多数据源特性

支持多数据库类型:例如,同时连接 MySQL、PostgreSQL 和 Vertica 等不同数据库,在应用中能够根据不同的业务需求切换到对应的数据库。

动态数据源切换:根据请求、业务逻辑或用户上下文,动态切换数据源(例如,按用户请求切换到不同的数据库),而不需要重启应用。

数据库隔离:不同业务模块使用不同的数据源,确保它们的数据完全隔离。例如,一个模块使用 MySQL,另一个模块使用 Vertica。

高可用和容错:通过配置多个数据源,可以在主数据库发生故障时自动切换到备用数据库,提升系统的高可用性。

实现

基于springboot3.4配置

引入依赖的方式

添加依赖
        <!--动态数据源-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>4.3.0</version>
        </dependency>
配置YML文件
spring:
  datasource:
    dynamic:
      # 设置默认的数据源或者数据源组,默认值即为 master
      primary: master
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
          username: root
          password: 123456
        slave:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/eshopping?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
          username: root
          password: 123456
调用数据库

直接再类上加上@DS(“master”)注解即是连接的master数据库

@Service
@DS("master")
public class LoginServiceImpl implements LoginService{
问题

博主这里遇见了问题,因为我配置了JWT+SpringSecurity,这里报错了,信息如下,找不到数据源,猜想应该是和JwtAuthenticationFilter和数据源的加载顺序有关

2025-01-21 21:43:37.097 ERROR --- [           main] o.s.b.web.embedded.tomcat.TomcatStarter  : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'jwtAuthenticationFilter': Unsatisfied dependency expressed through field 'userDetailsService': Error creating bean with name 'customUserDetailsService': Unsatisfied dependency expressed through field 'loginMapper': Error creating bean with name 'loginMapper' defined in file [E:\IDE\demoNew\target\classes\com\example\demonew\mapper\loginMapper\LoginMapper.class]: Cannot resolve reference to bean 'sqlSessionTemplate' while setting bean property 'sqlSessionTemplate'
2025-01-21 21:43:37.134 INFO  --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2025-01-21 21:43:37.155 WARN  --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Unable to start web server
2025-01-21 21:43:37.167 INFO  --- [           main] .s.b.a.l.ConditionEvaluationReportLogger : 

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2025-01-21 21:43:37.210 ERROR --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (the profiles dev are currently active).

手动实现

依赖默认已经引入,就是基础的mysql,mybatis依赖

配置YML文件
spring:
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
      username: root
      password: 123456
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/eshopping?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
      username: root
      password: 123456
配置DataSourceContextHolder

创建DataSourceContextHolder 类通过ThreadLocal存储数据源信息

/*DataSourceContextHolder 类用于保存当前线程的数据库标识,通常使用 ThreadLocal 来保存当前的数据源。*/
public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static String getDataSourceType() {
        return contextHolder.get();
    }

    public static void clearDataSourceType() {
        contextHolder.remove();
    }
}
创建DynamicDataSource

创建DynamicDataSource 类继承AbstractRoutingDataSource ,可获取当前线程数据源信息

/*DynamicDataSource 类继承自 AbstractRoutingDataSource,用于根据当前线程的上下文动态选择数据源。*/
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // 获取当前线程中的数据源标识
        return DataSourceContextHolder.getDataSourceType();
    }
}
配置DataSourceConfig

配置DataSourceConfig类,将配置文件的中的数据源配置成bean,然后存入DataSourceContextHolder 类中的ThreadLocal中存储

/*配置一个 DynamicDataSource,将多个 DataSource 放入其中,并设置默认数据源。*/
@Configuration
public class DataSourceConfig {

    @Primary
    @Bean(name = "masterDataSource")
    @ConfigurationProperties("spring.datasource.master")
    public HikariDataSource primaryDataSource() {
        return new HikariDataSource();
    }

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties("spring.datasource.slave")
    public HikariDataSource secondaryDataSource() {
        return new HikariDataSource();
    }

    @Bean
    public DataSource dataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("masterDataSource", primaryDataSource());
        targetDataSources.put("slaveDataSource", secondaryDataSource());

        DynamicDataSource routingDataSource = new DynamicDataSource();
        routingDataSource.setDefaultTargetDataSource(primaryDataSource());
        routingDataSource.setTargetDataSources(targetDataSources);

        return routingDataSource;
    }

}
使用

在serviceImpl中使用
主要是用下面的方法切换数据源DataSourceContextHolder.setDataSourceType(“masterDataSource”);

@Service
public class LoginServiceImpl implements LoginService{

    @Autowired
    private LoginMapper loginMapper;

    @Override
    public String register(String username, String password) {
        return "成功";
    }

    @Override
    public String login(String username, String password) {
        DataSourceContextHolder.setDataSourceType("masterDataSource");
        String pw = loginMapper.login(username);
        if(password.equals(pw)){
            return "成功";
        }else {
            return "失败";

        }
    }


}
清除

ThreadLocal不使用之后需要remove,不然容易造成数据污染或错误

原因

ThreadLocal 是与线程绑定的,在 Spring 或类似的框架中,线程池通常会重用线程,如果在请求处理过程中没有清理 ThreadLocal 中的内容,接下来复用的线程可能会带着之前请求中的 ThreadLocal 数据继续处理新的请求,从而导致数据污染或错误

假设你有两个数据源:primary 和 secondary,当请求 A 处理时,ThreadLocal 设置了 primary 数据源。
如果线程 A 继续处理请求 B,但此时没有清理 ThreadLocal,那么请求 B 可能会意外地使用 primary 数据源,而不是它本该使用的 secondary 数据源。

手动实现(注解版本)推荐

上面的版本每次都需要DataSourceContextHolder.setDataSourceType(“masterDataSource”);切换数据源非常麻烦,因此可以自定义注解

自定义注解

使用@DataSource时默认值masterDataSource

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "masterDataSource"; // 默认是 master 数据源
}

定义AOP切面

在使用该注解前将值赋值给DataSourceContextHolder中ThreadLocal存储,并可以方便的清除数据源

@Aspect
@Component
public class DataSourceAspect {

    @Before("@annotation(dataSource)")
    public void switchDataSource(DataSource dataSource) {
        String dataSourceType = dataSource.value();
        DataSourceContextHolder.setDataSourceType(dataSourceType);
    }

    @After("@annotation(dataSource)")
    public void clearDataSource(DataSource dataSource) {
        DataSourceContextHolder.clearDataSourceType();
    }

    @AfterThrowing("@annotation(dataSource)")
    public void handleException(DataSource dataSource) {
        DataSourceContextHolder.clearDataSourceType();
    }
}
使用
@Service
public class LoginServiceImpl implements LoginService{

    @Autowired
    private LoginMapper loginMapper;

    @Override
    public String register(String username, String password) {
        return "成功";
    }

    @Override
    @DataSource("slaveDataSource")
    public String login(String username, String password) {
        String pw = loginMapper.login(username);
        if(password.equals(pw)){
            return "成功";
        }else {
            return "失败";

        }
    }


}
;