Spring配置多数据源并实现动态切换

在业务增长到一定数量后,我们的数据库会产生大量的数据,而随着数据量的增大,单表或单库的性能已经达到瓶颈,我们会对数据库进行读写分离、分库分表等优化,而Spring提供的多数据源及动态切换在这些场景就可以派上用场了。

介绍一下AbstractRoutingDataSource

  1. AbstractRoutingDataSource是Spring中的一个抽象类,它的类继承关系如下,查看类注释信息:它是DataSource的抽象实现,根据查找key将getConnection()方法调用路由到多个目标数据源中的一个,后者通常是通过线程绑定的事务上下文确定的。

    Abstract DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.

  2. AbstractRoutingDataSource的关键属性和方法

    • afterPropertiesSet():在Spring容器填充bean属性后调用,解析目标数据源集合和默认目标数据源

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      private Map<Object, DataSource> resolvedDataSources;

      private DataSource resolvedDefaultDataSource;

      public void afterPropertiesSet() {
      if (this.targetDataSources == null) {
      throw new IllegalArgumentException("Property 'targetDataSources' is required");
      }

      // 对目标数据源集合和默认目标数据源进行解析,并填充到resolvedDataSources和resolvedDefaultDataSource属性
      this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
      this.targetDataSources.forEach((key, value) -> {
      Object lookupKey = resolveSpecifiedLookupKey(key);
      DataSource dataSource = resolveSpecifiedDataSource(value);
      this.resolvedDataSources.put(lookupKey, dataSource);
      });
      if (this.defaultTargetDataSource != null) {
      this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
      }
      }
    • getConnection():获取数据库连接

      1
      2
      3
      public Connection getConnection() throws SQLException {
      return determineTargetDataSource().getConnection();
      }
    • determineTargetDataSource():确定目标数据源

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      protected DataSource determineTargetDataSource() {
      Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
      // 获取查找key
      Object lookupKey = determineCurrentLookupKey();
      // 根据查找key从已解析数据源集合中获取目标数据源
      DataSource dataSource = this.resolvedDataSources.get(lookupKey);
      if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
      }
      if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
      }
      return dataSource;
      }
    • determineCurrentLookupKey():确定当前查找key,不限类型,但是需要和resolvedDataSources中的key类型一致

      ==抽象方法,需要子类重写==

      1
      2
      3
      private Map<Object, DataSource> resolvedDataSources;

      protected abstract Object determineCurrentLookupKey();

具体步骤

  1. 创建一个Spring Boot项目(只要是Spring项目就可以),引入MySQL驱动、MyBatis、druid、aop(使用aop切面编程实现数据源动态切换)等依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
    </dependency>

    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    <version>8.0.28</version>
    </dependency>

    <!--可以不用druid-spring-boot-starter,因为我们需要自定义数据源,用不上自动配置-->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.22</version>
    </dependency>
  2. 创建UserController、UserService、UserDao和UserMapper.xml(个人喜欢自己写SQL语句,也可以用MP等ORM框架),代码省略,只需要写一个查询接口就可以了

  3. 在两个数据库中添加两张结构一样的表,各添加一条数据,这里用nickname字段值不同代替主库和从库区别

  4. 创建application.yml或application.properties配置文件,添加数据库连接信息,这里如果用主从数据库连接更好,为了简化操作,这里我用两个不同的数据库代替主数据库和从数据库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    ds_01:
    username: root
    password: password1
    url: jdbc:mysql://host1:3306/practice?useUnicode=true&characterEncoding=utf-8&useSSL=false
    ds_02:
    username: root
    password: password2
    url: jdbc:mysql://host2:3306/practice?useUnicode=true&characterEncoding=utf-8&useSSL=false
  5. 创建DataSourceHolder,使用ThreadLocal管理查找key,和线程绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class DataSourceHolder {

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

    public static void set(String name) {
    DATASOURCE_HOLDER.set(name);
    }

    public static String get() {
    return DATASOURCE_HOLDER.get();
    }

    public static void clear() {
    DATASOURCE_HOLDER.remove();
    }

    }
  6. 创建DynamicDataSource,继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法,通过DataSourceHolder获取查找key

    1
    2
    3
    4
    5
    6
    7
    8
    public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
    return DataSourceHolder.get();
    }

    }
  7. 创建DataSourceEnum保存数据源信息(可以直接用字符串代替)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public enum DataSourceEnum {

    /**
    * 主数据源
    */
    MASTER("master"),
    /**
    * 从数据源
    */
    SLAVE("slave");

    private String name;

    DataSourceEnum(String name) {
    this.name = name;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }
    }
  8. 创建DataSourceConfig,配置数据源(可配置任意多个),其中dataSource01为主库数据源,dataSource02为从库数据源(实际不是),dynamicDataSource为[步骤6]中自定义的数据源,并且是主数据源(使用@Primary注解)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    @Configuration
    public class DataSourceConfig {

    @Resource
    private Environment environment;

    @Bean
    public DataSource dataSource01() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUsername(environment.getProperty("spring.ds_01.username"));
    dataSource.setPassword(environment.getProperty("spring.ds_01.password"));
    dataSource.setUrl(environment.getProperty("spring.ds_01.url"));

    return dataSource;
    }

    @Bean
    public DataSource dataSource02() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUsername(environment.getProperty("spring.ds_02.username"));
    dataSource.setPassword(environment.getProperty("spring.ds_02.password"));
    dataSource.setUrl(environment.getProperty("spring.ds_02.url"));

    return dataSource;
    }

    @Bean
    @Primary
    public DynamicDataSource dynamicDataSource() {
    DynamicDataSource dynamicDataSource = new DynamicDataSource();

    // 将主数据源和从数据源添加到目标数据源集合,设置默认目标数据源
    Map<Object, Object> targetDataSource = new HashMap<>(2);
    targetDataSource.put(DataSourceEnum.MASTER.getName(), dataSource01());
    targetDataSource.put(DataSourceEnum.SLAVE.getName(), dataSource02());

    dynamicDataSource.setDefaultTargetDataSource(dataSource01());
    dynamicDataSource.setTargetDataSources(targetDataSource);

    return dynamicDataSource;
    }

    }
  9. 创建自定义注解SwitchDataSource,控制使用哪个数据源

    1
    2
    3
    4
    5
    6
    7
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface SwitchDataSource {

    DataSourceEnum type() default DataSourceEnum.MASTER;

    }
  10. 创建DynamicDataSourceAspect切面,拦截带有@SwitchDataSource注解的方法,获取注解的name属性作为查找key

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Component
    @Aspect
    public class DynamicDataSourceAspect {

    @Around("@annotation(switchDataSource)")
    public Object around(ProceedingJoinPoint joinPoint, SwitchDataSource switchDataSource) {
    DataSourceEnum type = switchDataSource.type();

    DataSourceHolder.set(type.getName());
    try {
    return joinPoint.proceed();
    } catch (Throwable e) {
    throw new RuntimeException(e);
    } finally {
    DataSourceHolder.clear();
    }
    }

    }
  11. 在UserService的查找方法上添加自定义注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    @SwitchDataSource(type = DataSourceEnum.SLAVE)
    // @SwitchDataSource
    public UserDO getById(Long id) {
    UserDO user = userMapper.getById(id);

    ResponseEnum.USER_NOT_EXIST.assertNotNull(user);

    return user;
    }
  12. 对不使用注解、使用注解(不设置type)、使用注解(设置type为slave)三种情况进行测试,查看测试结果

    • 情况1、情况2

    • 情况3

至此,我们就完成了Spring多数据源配置和动态切换了,关于多数据源的事务问题会在后面的篇章中探究。