盘点动态切换数据源在实际项目中的应用

216 阅读6分钟

前言

最近在网上看到一篇介绍如何实现多数据源动态切换的文章,一时兴起,笔者便观察了一下目前公司代码对这块的应用,然后发现虽然不同微服务使用的数据库基本都是主从架构,但是它们对于数据源动态切换的实现方式都不一样,于是想着盘点总结一下。

场景应用盘点

场景1:一种库 + 一主一从 + MyBatis

架构

一种库,即只存在一种数据库,同时这个数据库拥一台主库和一台从库,整体架构如下图所示:

image.png

目标

执行SQL的时候自动切换主库或从库,实现读写分离

配置文件

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://xxxxx:3307/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver

代码

定义一个动态数据源类实现AbstractRoutingDataSource接口,在determineCurrentLookupKey方法中通过获取DubboProviderFilter携带的值来实现动态切换数据源,也就是说,使用方可以通过DubboProviderFilter来指定使用主库还是从库,而DubboProviderFilter内部其实就是使用了ThreadLocal。

/**
 * 根据AbstractRoutingDataSource路由到不同数据源中
 **/
public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(targetDataSources);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        if (Boolean.parseBoolean(DubboProviderFilter.getValue("readonly"))) {
            return "slave";
        }
        return "master";
    }
}

接着构建数据源即可

/**
 * 构建数据源
 **/
@Configuration
public class DateSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource createDynamicDataSource(){
        Map<Object,Object> dataSourceMap = new HashMap<>();
        DataSource defaultDataSource = masterDataSource();
        dataSourceMap.put("master",defaultDataSource);
        dataSourceMap.put("slave",slaveDataSource());
        return new DynamicDataSource(defaultDataSource,dataSourceMap);
    }
}

场景2:一种库 + 一主一从 + Sharding-JDBC + MyBatis

架构

场景2和场景1在架构上完全一致,只不过多用了Sharding-JDBC框架。

目标

执行SQL的时候自动切换主库或从库,实现读写分离。

配置文件

spring:
  # Sharding-JDBC的配置
  shardingsphere:
    datasource:
      # 数据源,这里配置两个
      names: master,slave0
      # 主库
      master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://xxxxxxxx:3306/db1?serverTimezone=GMT%2B8&characterEncoding=utf-8
        username: root
        password: 123456
      # 从库
      slave0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://xxxxxxxx:3307/db1?serverTimezone=GMT%2B8&characterEncoding=utf-8
        username: root
        password: 123456

    # 主从节点配置
    masterslave:
      # 从库负载均衡算法,内置两个值:RANDOM、ROUND_ROBIN
      load-balance-algorithm-type: round_robin
      # 主从的名称,要保证唯一
      name: ms
      # 指定主数据源
      master-data-source-name: master
      # 指定从数据源(可以多个)
      slave-data-source-names: slave0

代码

不需要额外代码,配置好之后Sharding-JDBC便能自动解析SQL语句,如果是增删改则走主库,查询则走从库。

场景3:多种库 + 一主一从 + MyBatis

架构

多种库,顾名思义,即多个拥有不同数据表的库,而且这些库都有主库和从库。在我们的系统中有个模块专门负责提供报表查询服务,因此该应用要同时和不同业务的数据库建立连接。 image.png

目标

执行SQL的时候能够切换到对应的库,同时实现读写分离

配置文件

这里示例配置3种库,分别是order(订单)、user(用户)、promotion(营销)

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      order-master:
        url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      order-slave:
        url: jdbc:mysql://xxxxx:3307/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      user-master:
        url: jdbc:mysql://xxxxxx:3306/test3?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      user-slave:
        url: jdbc:mysql://xxxxx:3307/test4?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver      
      promotion-master:
        url: jdbc:mysql://xxxxxx:3306/test3?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      promotion-slave:
        url: jdbc:mysql://xxxxx:3307/test4?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
        

代码

先实现AbstractRoutingDataSource接口;同时内置一个ThreadLocal,用来存放每次执行SQL语句的时候选中的数据源名称

public class DynamicDataSource extends AbstractRoutingDataSource {
    //线程本地环境
   private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

  /**
   * 构造器
   */
  public DynamicDataSource(DataSource defaultTargetDataSource, Map < Object, Object > targetDataSources) {
    super.setDefaultTargetDataSource(defaultTargetDataSource);
    super.setTargetDataSources(targetDataSources);
    super.afterPropertiesSet();
  }

  /**
   * 路由数据源
   */
  @Override
  protected Object determineCurrentLookupKey() {
    return getDataSource();
  }

  /**
   * 设置ThreadLocal数据源
   */
  public static void setDataSource(String dataSource) {
    contextHolder.set(dataSource);
  }

  /**
   * 获取ThreadLocal数据源
   */
  public static String getDataSource() {
    return contextHolder.get();
  }

  /**
   * 清除ThreadLocal数据源
   */
  public static void clearDataSource() {
    contextHolder.remove();
  }

}

构建多个数据源
@Configuration
public class DynamicDataSourceConfig {

  @Bean
  @ConfigurationProperties("spring.datasource.druid.order-master")
  public DataSource orderMasterDataSource() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.druid.order-slave")
  public DataSource orderSlaveDataSource() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.druid.user-master")
  public DataSource userMasterDataSource() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.druid.user-slave")
  public DataSource userSlaveDataSource() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.druid.promotion-master")
  public DataSource promotionMasterDataSource() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.druid.promotion-slave")
  public DataSource promotionSlaveDataSource() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean
  @Primary
  public DynamicDataSource dataSource(DataSource orderMasterDataSource, DataSource orderSlaveDataSource,
    DataSource userMasterDataSource, DataSource userSlaveDataSource,
    DataSource promotionMasterDataSource, DataSource promotionSlaveDataSource
  ) {
    Map <Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put("order-master", orderMasterDataSource);
    targetDataSources.put("order-slave", orderSlaveDataSource);

    targetDataSources.put("user-master", userMasterDataSource);
    targetDataSources.put("user-slave", userSlaveDataSource);

    targetDataSources.put("promotion-master", promotionMasterDataSource);
    targetDataSources.put("promotion-slave", promotionSlaveDataSource);
    return new DynamicDataSource(orderSlaveDataSource, targetDataSources);
  }
}

自定义注解,用来标注dao层的某个方法或者类使用哪个数据源
@Target({
  ElementType.TYPE,
  ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSources {
  String name() default "order-slave";
}

使用AOP切面结合上述自定义注解进行动态切换数据源。注解指定了对应的数据源,但是最终是通过方法名来决定选择主库还是从库。

@Aspect
@Component
public class DataSourceAspect implements Ordered {
  /**
   * dao层切面
   */
  @Pointcut("execution(* com.xxx.xxx.xxx.dao..*(..))")
  public void dao() {

  }

  @Around("dao()")
  public Object around(ProceedingJoinPoint point) throws Throwable {
    try {
      // 数据源优先级:  1、方法上的  2、类上的
      MethodSignature signature = (MethodSignature) point.getSignature();
      Class clazz = (Class) AopUtils.getTargetClass(point.getTarget()).getGenericInterfaces()[0];
      resolveDataSource(clazz, signature.getMethod());
      return point.proceed();
    } finally {
      DynamicDataSource.clearDataSource();
    }
  }

  /**
   * 提取目标对象方法注解和类型注解中的数据源标识
   */
  private void resolveDataSource(Class<?> clazz, Method method) throws Throwable {
    // 默认使用订单从库
    String sourceName = "order-slave";

    // 类上的注解
    DataSources source = clazz.getAnnotation(DataSources.class);
    if (source != null) {
      sourceName = source.name();
    }

    // 方法上的注解
    if (method.isAnnotationPresent(DataSources.class)) {
      source = method.getAnnotation(DataSources.class);
      if (source != null) {
        sourceName = source.name();
      }
    }

    if (method.getName().startsWith("update") ||
      method.getName().startsWith("batchUpdate") ||
      method.getName().startsWith("batchWithBatch") ||
      method.getName().startsWith("insert") ||
      method.getName().startsWith("batchInsert") ||
      method.getName().startsWith("batchDelete") ||
      method.getName().startsWith("saveBatch") ||
      method.getName().startsWith("set") ||
      method.getName().startsWith("delete")) {
      // 增删改语句则走主库
      sourceName = sourceName.replace("slave", "master");
    }
    DynamicDataSource.setDataSource(sourceName);
  }

  @Override
  public int getOrder() {
    return 1;
  }
}

dao层的mapper文件如下
@DataSources(name = "order-slave")
public interface OrderMapper {

  @DataSources(name = "order-master")
  List<StatOrderDailyPO> getDailyStatOrderByTimeRange(OrderDailyReqModel model);

  @DataSources(name = "order-slave")
  List<StatOrderDailyPO> countDailyStatOrderByTimeRange(OrderDailyReqModel model);

  // 会被AOP切换为主库
  int insertDailyStatOrderInfo(List<StatOrderDailyPO> list);
}

总结

3种场景,简单的说,要么实现AbstractRoutingDataSource接口,要么直接使用Sharding-JDBC框架。

通过对比上述场景1和场景2,可以看出,对于一种数据库并且是主从架构的情况,使用Sharding-JDBC效率更高,不用自己去实现读写分离数据源切换;而且如果是一主多从,Sharding-JDBC还支持对多个从库进行负载均衡。

笔者这里只列举了3种自己公司项目中使用多数据源的方法,并非所有场景都能适用这些方案,也希望这些例子能够帮到大家。