阅读 143

SpringBoot实现读写分离

需求

  假设项目中有一个几千万数据的大表,业务写入和读取都比较频繁。这个表随着数据的增多,数据库单表执行缓慢,逐渐成为应用瓶颈。所以需要迫切的解决这个问题。

思路

一般我们会有如下几个方案:  

  1. 搭建一个只读从库,实现SpringBoot多数据源。(成本最小、对业务代码几乎无影响)
  2. 分库分表,引入sharding-jdbc,或独立部署的sharding-proxy。(引入成本高、分表需要改造sql代码)
  3. 引入Mycat,利用Mycat实现读写分离。(引入成本高,增加系统复杂度)

综合思考后,利用SpringBoot多数据源实现读写分离改造成本最低,对业务代码也基本无影响,而且可以对部分不需要读写分离的业务场景做灵活处理。

编码实现

  一般项目会引入阿里开源的Druid管理数据库连接池,所以我们需要重新定义Druid的配置类,注入读和写两个数据源,实现AbstractRoutingDataSource数据源切换,定义切面在代码执行时切换数据源。

1.重写Druid的DataSource封装类

  为了获取自定义数据源配,我们需要重写druid-spring-boot-starter的DataSource封装类,代码如下:

@ConfigurationProperties("spring.datasource.druid")
public class DruidDataSourceWrapperX extends DruidDataSource implements InitializingBean {
    @Autowired
    private DataSourceProperties basicProperties;

    /* 只读数据源 */
    private String url2;
    private String username2;
    private String password2;

    @Override
    public void afterPropertiesSet() throws Exception {
        if (super.getUsername() == null) {
            super.setUsername(StringUtils.isNotBlank(this.username2) ? this.username2 : this.basicProperties.determineUsername());
        }

        if (super.getPassword() == null) {
            super.setPassword(StringUtils.isNotBlank(this.password2) ? this.password2 : this.basicProperties.determinePassword());
        }

        if (super.getUrl() == null) {
            super.setUrl(StringUtils.isNotBlank(this.url2) ? this.url2 : this.basicProperties.determineUrl());
        }

        if (super.getDriverClassName() == null) {
            super.setDriverClassName(this.basicProperties.getDriverClassName());
        }
    }

    @Autowired(
            required = false
    )
    public void autoAddFilters(List<Filter> filters) {
        super.filters.addAll(filters);
    }

    @Override
    public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {
        try {
            super.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
        } catch (IllegalArgumentException var4) {
            super.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
        }
    }

    public void setUrl2(String url2) {
        this.url2 = url2;
    }

    public void setUserName2(String userName2) {
        this.username2 = userName2;
    }

    public void setPassword2(String password2) {
        this.password2 = password2;
    }
}
复制代码

2.继承AbstractRoutingDataSource实现动态路由

  Spring提供了AbstractRoutingDataSource,用户可以继承实现自己的动态路由切换,详细可以查看spring的官方文档。

public class DynamicDataSource extends AbstractRoutingDataSource {

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

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

    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(DBSourceEnum dbSourceEnum) {
        CONTEXT_HOLDER.set(dbSourceEnum);
    }

    public static DBSourceEnum getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}
复制代码

  定义数据源枚举类:

public enum DBSourceEnum {
    /**
     master datasource
    */
    MASTER,
    /**
     slave datasource
    */
    SLAVE
}
复制代码

3.实现DataSourceAutoconfig

  完成读和写两个数据的bean定义,代码如下:

@Configuration
@Component
public class DruidDataSourceAutoConfigureX {
  private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigureX.class);

  public DruidDataSourceAutoConfigureX() {
  }

  @Bean
  public DataSource masterDataSource() {
      LOGGER.info("Init Master DruidDataSource");
      return new DruidDataSourceWrapperX();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.slave")
  public DataSource slaveDataSource() {
      LOGGER.info("Init Slave DruidDataSource");
      return new DruidDataSourceWrapperX();
  }

  @Bean
  @Primary
  @DependsOn({"masterDataSource","slaveDataSource"})
  public DynamicDataSource myDataSource(DataSource masterDataSource,  DataSource slaveDataSource) {
      LOGGER.info("Init DynamicDataSource");
      Map<Object, Object> targetDataSources = new HashMap<>(2);
      targetDataSources.put(DBSourceEnum.MASTER, masterDataSource);
      targetDataSources.put(DBSourceEnum.SLAVE, slaveDataSource);
      return new DynamicDataSource(masterDataSource, targetDataSources);
  }
}
复制代码

4.定义切面实现运行时动态切切换数据源

  定义注解SlaveDataSource和切面DataSourceAspect,在dao层方法添加SlaveDataSource注解时,使用只读数据源。

@Target(ElementType.METHOD)
@RetentionPolicy.RUNTIME)
@Documented
public @interface SlaveDatasource{
    String name() default "SLAVE";
}
复制代码
@Aspect
@Component
public class DataSourceAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class);

    @Pointcut("@annotation(*.SlaveDatasource)" +
              "&& execution(* *.dao.*.*(..))")
    public void dataSourcePointCut(){}

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        SlaveDatasource slaveDatasource = method.getDeclaredAnnotation(SlaveDatasource.class);
        if (slaveDatasource != null) {
            LOGGER.info("Use Slave DataSource!");
            DynamicDataSource.setDataSource(DBSourceEnum.SLAVE);
        }
        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
        }
    }
}
复制代码

5.mybatis接口方法添加注解

示例:

@Repository
public interface orderMapper {
    @SlaveDatasource
    Order getOrderById(Long id);
}
复制代码

6.配置文件

  主数据源还是按spring.datasource配置,在此基础上增加salve的配置:

spring:
  datasource:
    slave:
      url2: xxx
      username2: xxx
      password: xxxx
复制代码

总结

  本次主要是利用Spring的数据源动态路由类和切面实现了读写分离,因为使用了druid连接池,所以druid相关配置类也做了重写。本文比较谨慎的使用注解判定是否使用只读数据源,读者也可以通过判断方法名是否含“query”等判定使用只读数据源,或根据业务灵活处理。

文章分类
后端