一次多数据源的开发经历(clickhouse+多个mysql数据源)

471 阅读3分钟

1.背景

使用技术:mybatis-plus动态数据源,使用的数据源有mysql+clickhouse和外部mysql数据源 引入jar包:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

2.自动装配动态数据源bean

@Configuration
@AutoConfigureBefore({DynamicDataSourceAutoConfiguration.class, SpringBootConfiguration.class})
@Slf4j
public class ExtendDynamicDataSourceAutoConfiguration  {
    
     /**
     * 动态数据源配置项
     */
    @Resource
    private DynamicDataSourceProperties properties;

    private DynamicRoutingDataSource dynamicRoutingDataSource;

    /**
     * 将动态数据源设置为首选的
     * 当spring存在多个数据源时, 自动注入的是首选的对象
     * 设置为主要的数据源之后,就可以支持shardingjdbc原生的配置方式了
     *
     * @return
     */
    @Primary
    @Bean
    public DynamicRoutingDataSource dynamicRoutingDataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setPrimary(properties.getPrimary());
        dataSource.setStrict(properties.getStrict());
        dataSource.setStrategy(properties.getStrategy());
        dataSource.setP6spy(properties.getP6spy());
        dataSource.setSeata(properties.getSeata());
        this.dynamicRoutingDataSource = dataSource;
        return dataSource;
    }
}

3.自定义数据源切换

此处是为了切换前加载数据源逻辑 定义切换注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Ds {

}
@Component
@Aspect
@Slf4j
public class DynamicDataSourceAspect {

    @Pointcut("@annotation(org.luoch.infrastructure.aspect.Ds)")
    private void pointcut() {}

    @Before("pointcut() && @annotation(ds)")
    public void before(JoinPoint joinPoint, Ds ds) {
        Object[] objects = joinPoint.getArgs();
        if(objects == null){
            return;
        }
        boolean flag = false;
        for(Object object : objects){
            if(object instanceof BaseModel){
                BaseModel baseModel = (BaseModel)object;
                handleRdsConfig(baseModel);
                flag = true;
                break;
            }
        }
        if(!flag){
            log.warn("方法入参中没有找到baseModel,进行切换数据源操作失败,请检查");
        }
    }

    private void handleMysqlConfig(BaseModel baseModel){
        MysqlConfigInfo rdsConfigInfo = baseModel.getMysqlConfigInfo();
        DataSourceProperty property = new DataSourceProperty();
        property.setUsername(rdsConfigInfo.getUserName());
        property.setPassword(rdsConfigInfo.getPassword());
        property.setUrl(rdsConfigInfo.getJdbcUrl());
        property.setDriverClassName("com.mysql.cj.jdbc.Driver");
        this.pushDataSource(property, rdsConfigInfo.getInstanceId());
    }

    @AfterReturning(pointcut="pointcut()", returning = "response")
    public void after(JoinPoint joinPoint, Object response) {
        this.clearDataSourceCache();
    }


}

此处为创建/创建并切换为该数据源。此处加了锁,原因是创建数据源为耗时操作,当有并发创建相同数据源的时候,动态数据源管理器会把原来的数据源先销毁在创建,源码在此处:

image.png

 /**
     * 向动态数据源中增加数据源
     * @param dataSourceProperty
     * @param name
     */
    public void pushDataSource(DataSourceProperty dataSourceProperty, String name){
        Assert.notNull(dynamicRoutingDataSource, "程序错误,数据源为空!");
        Assert.notNull(dataSourceProperty, "数据源配置不能为空!");
        Assert.notNull(name, "数据源名称不能为空!");
        DataSource dataSource;
        try {
            dataSource = dynamicRoutingDataSource.getDataSource(name);
        } catch (CannotFindDataSourceException e){
            dataSource = null;
        }
        if(dataSource == null){
            synchronized (this){
                try {
                    dataSource = dynamicRoutingDataSource.getDataSource(name);
                } catch (CannotFindDataSourceException e){
                    log.warn("从动态数据源中获取当前数据源为空,即将进行数据源创建操作!");
                }
                if(dataSource == null){
                    dataSource = sourceSupporter.createDataSource(dataSourceProperty);
                    dynamicRoutingDataSource.addDataSource(name, dataSource);
                }
            }
        }
        //缓存中手动切换到当前数据源
        DynamicDataSourceContextHolder.push(name);
        log.info("当前线程:{},已成功切换数据源,名称为:{},jdbc配置为:{}", Thread.currentThread().getName(),
                name, dataSourceProperty.getUrl());
    }

    /**
     * 去除缓存中的数据源
     */
    public void clearDataSourceCache(){
        DynamicDataSourceContextHolder.clear();
    }

4.默认数据源切换

直接使用注解@DS切换即可,比如:

@Mapper
@DS(DataSourceName.CLICKHOUSE)
public interface XxxMapper extends BaseMapper<XxxDo>  {
    
}

5. 向动态数据源中手动添加clickhouse数据源

原因:通过源码可知道,clickhouse 默认的jdbc驱动使用的是httpclient(apache),某些配置参数不受jdbc参数约束,所以需要手动添加数据源

image.png

  ...

private final ClickhousePoolConfig clickhousePoolConfig;

private final DynamicRoutingDataSource dataSource;

public DynamicDataSourceCollectorContainer(ClickhousePoolConfig clickhousePoolConfig, DynamicRoutingDataSource dataSource) {
    this.clickhousePoolConfig = clickhousePoolConfig;
    this.dataSource = dataSource;
} 

public void addDataSource() {
    dataSource.addDataSource(DataSourceName.CLICKHOUSE, getClickhouseDataSource());
}

private DataSource getClickhouseDataSource() {
    HikariConfig hikariConfig = new HikariConfig();
    hikariConfig.setUsername(clickhousePoolConfig.getUser());
    hikariConfig.setPassword(clickhousePoolConfig.getPassword());
    hikariConfig.setJdbcUrl(clickhousePoolConfig.getUrl());
    ClickHouseProperties ckProperties = new ClickHouseProperties();
    //设置clickhouse http客户端参数
    ckProperties.setSocketTimeout(clickhousePoolConfig.getQueryTimeOut());
    ckProperties.setCompress(clickhousePoolConfig.getEnableHttpCompression());
    ckProperties.setConnectionTimeout(clickhousePoolConfig.getConnectTimeout());
    //使用BalancedClickhouseDataSource是为了后期可能使用多个clickhouse数据源
    BalancedClickhouseDataSource ckDataSource = new BalancedClickhouseDataSource(clickhousePoolConfig.getUrl(), ckProperties);
    //...其他参数
    hikariConfig.setDataSource(ckDataSource);
    //将数据源放入hikari数据源
    return new HikariDataSource(hikariConfig);
}
   ....

加载此bean在动态数据源创建之后即可

//在动态数据源之后加载这个配置类
@AutoConfigureAfter(ExtendDynamicDataSourceAutoConfiguration.class)

  ...
  
  
@ConditionalOnMissingBean
@Bean
public ExtendDataSourceCollectorContainer billDataSourceCollectorContainer(ConsumerConfig properties,
                                                                      DynamicRoutingDataSource dynamicRoutingDataSource) {
    ExtendDataSourceCollectorContainer dataSourceCollectorContainer = new ExtendDataSourceCollectorContainer(properties.getClickhousePoolConfig(), dynamicRoutingDataSource);
    dataSourceCollectorContainer.addDataSource();
    return dataSourceCollectorContainer;
}

6. 总结

本文实现了动态数据源切换、通过切面加载外部数据源并切换、手动添加clickhouse数据源,满足了大部分需要动态数据源的场景