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();
}
}
此处为创建/创建并切换为该数据源。此处加了锁,原因是创建数据源为耗时操作,当有并发创建相同数据源的时候,动态数据源管理器会把原来的数据源先销毁在创建,源码在此处:
/**
* 向动态数据源中增加数据源
* @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参数约束,所以需要手动添加数据源
...
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数据源,满足了大部分需要动态数据源的场景