Spring Boot多数据源配置

893 阅读4分钟

本文介绍场景:

  1. Spring Boot多数据源配置
  2. 结合Druid实现多数据源配置

准备工作

  1. 两个一模一样的数据库:db1和db2,创建好表,比如Student

实现步骤

  1. application.ymls中配置两个数据库信息
  2. 自定义数据库配置类,排除SpringBoot提供的自动配置
  3. 关键在于SpringBoot的AbstractRoutingDataSource类,里面有个实现方法determineCurrentLookupKey,在这里动态的指定需要的数据源
  4. 结合ThreadLocal和AOP切面来实现数据源的动态切换

具体操作

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    db1: # 数据源1
      url: jdbc:mysql://mylocalhost:3306/db1?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: root
    db2: # 数据源2
      url: jdbc:mysql://mylocalhost:3306/db2?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: root

自定义数据源配置类,自定义后我们需要将SpringBoot内的数据源自动配置被排除掉:

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;
​
@Configuration
public class DataSourceConfig {
    @Bean(name = "db1")
    @ConfigurationProperties("spring.datasource.db1")
    public DataSource db1DataSource() {
        return DataSourceBuilder.create().build();
    }
​
    @Bean(name = "db2")
    @ConfigurationProperties("spring.datasource.db2")
    public DataSource db2DataSource() {
        return DataSourceBuilder.create().build();
    }
​
    @Bean(name = "dynamicDataSource")
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 配置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(db1DataSource());
​
        // 配置多数据源,后面才可以进行动态切换
        HashMap<Object, Object> datasourceMap = new HashMap<>();
        datasourceMap.put(DataSourceEnum.DATASOURCE1.name(), db1DataSource());
        datasourceMap.put(DataSourceEnum.DATASOURCE2.name(), db2DataSource());
        dynamicDataSource.setTargetDataSources(datasourceMap);
​
        return dynamicDataSource;
    }
}

排除自动配置,不然会有循环引用:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

创建一个枚举便于数据源切换,不用写魔法值:

public enum DataSourceEnum {
    /**
     * 数据源1
     */
    DATASOURCE1,
​
    /**
     * 数据源2
     */
    DATASOURCE2,
}

利用ThreadLocal创建线程安全的变量副本,记录当前线程用的是哪个数据源:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
​
public class DataSourceContextHolder {
    private static final Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
​
    /**
     * 使用ThreadLocal线程安全的使用变量副本
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
​
    /**
     * 设置/指定数据源
     */
    public static void setDataSource(String dataSource) {
        log.info("切换到数据源:{}", dataSource);
        CONTEXT_HOLDER.set(dataSource);
    }
​
    /**
     * 获取数据源
     */
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }
​
    /**
     * 清空信息
     */
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

创建注解来声明数据源:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    DataSourceEnum value() default DataSourceEnum.DATASOURCE1;
}

创建该注解的切面,读取要用到的数据源:

@Aspect
@Component
@Order(-1)
public class DataSourceAspect {
    @Pointcut("@annotation(com.cc.config.DataSource)")
    public void dataSourcePointCut() {}
​
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if (dataSource != null) {
            DataSourceContextHolder.setDataSource(dataSource.value().name());
        }
​
        try {
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

切面顺序务必比@Transactional注解的切面高,不然拿不到该数据源,也就没办法实现切换了

@Transactional注解的切面是最大值,即执行顺序最低,所以@Order(-1)就可以

最后是给需要的接口方法添加注解,声明使用的数据源,不加则是默认数据源:

@DataSource(value = DataSourceEnum.DATASOURCE2)
@RequestMapping(value = "/student/listAll", method = RequestMethod.GET)
public List<Student> listAll() {
    return studentService.listAll();
}

结合Druid

添加依赖:

<!--druid连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.21</version>
</dependency>

配置文件要修改一下:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      db1:
        url: jdbc:mysql://mylocalhost:3306/db1?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
      db2:
        url: jdbc:mysql://mylocalhost:3306/db2?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
      # 初始化时建立物理连接的个数
      initial-size: 5
      # 最大连接池数量
      max-active: 20
      # 最小连接池数量
      min-idle: 10
      # 获取连接时最大等待时间,单位毫秒
      max-wait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 连接保持空闲而不被驱逐的最小时间
      min-evictable-idle-time-millis: 300000
      # 用来检测连接是否有效的sql,要求是一个查询语句
      validation-query: SELECT 1 FROM DUAL
      # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
      test-while-idle: true
      # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
      test-on-borrow: false
      # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
      test-on-return: false
      # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭
      pool-prepared-statements: false
      # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true
      max-pool-prepared-statement-per-connection-size: 50
      # 配置监控统计拦截的filters,去掉后监控界面sql无法统计
      filter:
        stat:
          enabled: true
          # 慢sql记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
      # 合并多个DruidDataSource的监控数据
      use-global-data-source-stat: true
      web-stat-filter:
        enabled: true
      stat-view-servlet:
        enabled: true
        # 设置白名单,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # 用户名密码
#        login-username: admin
#        login-password: 123456

自定义配置类也要调整,给DataSourceBuilder创建的时候添加上type类型:

@Configuration
public class DataSourceConfig {
    @Bean(name = "db1")
    @ConfigurationProperties("spring.datasource.druid.db1")
    public DataSource db1DataSource() {
        return DataSourceBuilder.create().type(DruidDataSource.class).build();
    }
​
    @Bean(name = "db2")
    @ConfigurationProperties("spring.datasource.durid.db2")
    public DataSource db2DataSource() {
        return DataSourceBuilder.create().type(DruidDataSource.class).build();
    }
​
    @Bean(name = "dynamicDataSource")
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 配置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(db1DataSource());
​
        // 配置多数据源
        HashMap<Object, Object> datasourceMap = new HashMap<>();
        datasourceMap.put(DataSourceEnum.DATASOURCE1.name(), db1DataSource());
        datasourceMap.put(DataSourceEnum.DATASOURCE2.name(), db2DataSource());
        dynamicDataSource.setTargetDataSources(datasourceMap);
​
        return dynamicDataSource;
    }
}

其他的保持不变即可,启动后访问:http://localhost:8080/druid/

思考

多数据源的应用场景一般用于单体应用需要访问多个数据库的情况,

或者为了减轻主数据库压力,将一定的访问量分配给从数据库。

如果想要做到类似Ribbon那样的负载均衡,让从数据库动态的承担访问压力,而不是指定死某些接口,那其实做成微服务的形式就好,没有必要自己撸一个出来,但也可以看看有没有现成的轮子可以用。

参考资料

Spring Boot 中的多数据源配置方案

Springboot多数据源配置详解