在Spring boot项目中实现多数据源的方案

在实际项目开发过程中,因为业务发展和技术实现的需要,我们会在项目中同时连接多个数据库,具体该怎么做呢?

  • 目标
    1. 假设有三个MySQL8数据库,在项目中需要同时连接这些库,来分别处理不同的业务。
    2. 默认使用第一个数据库(主库),可以通过多种方式动态切换数据源,满足业务不同场景下的需求。
  • 在配置文件(application.yml)中配置数据库连接信息
spring:
  application:
    name: springboot-multisources
  datasource:
    primary:
      pool-name: primary-test_01
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://${MYSQL_HOST:localhost}:3306/test_01
      username: root
      password: root
    secondary:
      pool-name: secondary-test_02
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://${MYSQL_HOST:localhost}:3306/test_02
      username: root
      password: root
    third:
      pool-name: secondary-test_03
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://${MYSQL_HOST:localhost}:3306/test_03
      username: root
      password: root

  • 配置多个数据源,并定义一个动态数据源。
@Configuration
public class DataSourceConfig {

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

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

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

    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource,
                                        @Qualifier("secondaryDataSource") DataSource secondaryDataSource,
                                        @Qualifier("thirdDataSource") DataSource thirdDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(3);
        targetDataSources.put(DynamicDataSource.Type.primary.name(), primaryDataSource);
        targetDataSources.put(DynamicDataSource.Type.secondary.name(), secondaryDataSource);
        targetDataSources.put(DynamicDataSource.Type.third.name(), thirdDataSource);

        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setDefaultTargetDataSource(primaryDataSource);
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }
    
}
  • 写一个注解便于手动切换
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicDataSource {

    /**
     * 要切换数据库的标识
     *
     * @return
     */
    Type value() default Type.primary;

    enum Type {
        /**
         * 主库
         */
        primary,
        /**
         * 第二个库
         */
        secondary,
        /**
         * 第三个库
         */
        third
    }
}
  • 定义一个动态数据源,继承 AbstractRoutingDataSource,通过重写 determineCurrentLookupKey 方法来实现动态数据源切换。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}
  • 动态切换数据库应该与每个请求关联,也就是说每个请求都应维护一个自己使用的数据源。
    • ThreadLocal 是一个线程本地存储,它提供了一个简单的方法,可以将数据与当前线程关联起来。 在 Spring 中,可以使用 ThreadLocal 来存储当前线程使用的数据源信息。
    • 在处理每个请求时,可以将数据源信息存储在 ThreadLocal 中,在处理完请求后,再将其从 ThreadLocal 中删除。
public class DynamicDataSourceContextHolder {

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

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

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

    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}
  • 解决异步执行时上下文传递的问题

    • JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。
    • 大家可以选择阿里开源的TransmittableThreadLocal(TTL):在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
  • 定义一个拦截器,在请求时检查Controller类和方法上的注解,根据配置的值设置当前请求用什么数据源。在请求响应回去时清除当前线程绑定的数据,做一个资源的释放动作。

public class DataSourceInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        DynamicDataSource.Type type = null;
        // 从controller类的方法上找注解
        HandlerMethod handlerMethod = ((HandlerMethod) handler).getResolvedFromHandlerMethod();
        if (null != handlerMethod) {
            DynamicDataSource methodAnnotation = handlerMethod.getMethodAnnotation(DynamicDataSource.class);
            if (null != methodAnnotation) {
                type = methodAnnotation.value();
            }
        }
        // 如果上面找不到,则尝试到类头上找注解。
        if (null == type) {
            DynamicDataSource classAnnotation = ((HandlerMethod) handler).getBean().getClass().getAnnotation(DynamicDataSource.class);
            type = classAnnotation.value();
        }
        // 如果有指定数据源就切换,否则使用默认的。
        if (null != type) {
            DynamicDataSourceContextHolder.setDataSourceKey(type.name());
        }
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        DynamicDataSourceContextHolder.clearDataSourceKey();
    }
}
  • 在配置类中注册拦截器
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new DataSourceInterceptor());
    }
}
  • 在Controller类中加上注解来切换源
@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserService userService;

    @DynamicDataSource(DynamicDataSource.Type.secondary)
    @GetMapping("users")
    List<User> users() {
        return userService.listUsers();
    }
}
  • 结合上面的示例思考,如果想要Service层切换数据源该怎么做呢?
  • 我使用的示例项目的依赖文件:
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.8'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    maven {
        url 'https://maven.aliyun.com/repository/public/'
    }
    maven {
        url 'https://maven.aliyun.com/repository/spring/'
    }
    maven {
        url 'https://maven.aliyun.com/repository/gradle-plugin'
    }
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'com.mysql:mysql-connector-j'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

你可能感兴趣的:(java,spring,boot,java,数据库)