一、需求背景
最近团队需要做一个需求,可能会从多个数据源中抽取数据,然后经过清洗、转换等生成统计报告。因此我们的项目需要对接多个数据源,并且需要满足以下要求:
1、多个请求同时到达,每个请求可能访问不同的数据库,请求间应该隔离,不能阻塞
2、数据源信息能做到好维护,并且支持动态添加(添加之后,代码能感应到)
二、方案设计
A、AbstractRoutingDataSource
spring中abstract的类大部分都是可扩展的,在spring中操作数据源一般都是要基于orm框架,例如mybatis啥的,模型如下:
但是现在会有多个数据源,一种比较好点的方式是多个数据源共用一个sessionFactory,如下:
这样后面就算是增加数据源,变化也是非常小的。
再说回到abstractRoutingDataSource,可以理解为就是一个数据源的容器,里面分成了默认数据源还有外部数据源,外部数据源通过Map维护,通过数据源名称可以找到对应的数据源。
B、AOP
系统启动的时候,加载默认数据库(项目自己的数据源,非外部数据源)。等到有请求进来的时候,通过aop拦截判断数据源是否已加载,若未加载,加载一次
三、代码实现
A、数据表结构
B、数据源配置
package com.yunzhangfang.platform.dataplatform.dw.service.datasource.dynamic; import com.yunzhangfang.platform.dataplatform.dw.service.datasource.holder.DynamicDataSourceContextHolder; import com.yunzhangfang.platform.dataplatform.dw.service.domain.DatasourceConfigDO; import com.yzf.accounting.common.exception.BizRuntimeException; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.sql.DataSource; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 动态数据源 */ @Slf4j @Data public class DynamicDataSource extends AbstractRoutingDataSource { // 默认数据源 private Object defaultTargetDataSource; // 外部数据源 private Map<Object, Object> targetDataSources; @Override protected Object determineCurrentLookupKey() { String datasource = DynamicDataSourceContextHolder.getDataSource(); if(StringUtils.isBlank(datasource)) { log.info("默认数据源"); return datasource; } if(!this.targetDataSources.containsKey(datasource)) { throw new BizRuntimeException("不存在此数据源"); } return datasource; } /** * 设置默认数据源 * @param defaultTargetDataSource */ @Override public void setDefaultTargetDataSource(Object defaultTargetDataSource) { super.setDefaultTargetDataSource(defaultTargetDataSource); this.defaultTargetDataSource = defaultTargetDataSource; } /** * 设置外部数据源 * @param targetDataSources */ @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { super.setTargetDataSources(targetDataSources); this.targetDataSources = targetDataSources; } /** * 创建数据源 * @return */ public boolean createDataSource(List<DatasourceConfigDO> datasourceConfigList) { if(CollectionUtils.isEmpty(datasourceConfigList)) { return false; } Map<Object, Object> dataSourceMap = new HashMap<>(); datasourceConfigList.forEach(datasourceConfig -> { DataSource dataSource = null; try { dataSource = DataSourceBuilder.create() .driverClassName(datasourceConfig.getDsDriver()) .url(datasourceConfig.getDsUrl()) .username(datasourceConfig.getDsUsername()) .password(datasourceConfig.getDsPassword()) .type((Class<? extends DataSource>) Class.forName(datasourceConfig.getDsType())) .build(); } catch (ClassNotFoundException e) { log.error("创建数据源出现异常", e); } dataSourceMap.put(datasourceConfig.getDsName(), dataSource); }); this.targetDataSources.putAll(dataSourceMap); setTargetDataSources(this.targetDataSources); super.afterPropertiesSet(); log.info("数据源创建成功"); return true; } }
package com.yunzhangfang.platform.dataplatform.dw.service.datasource.config; import com.yunzhangfang.platform.dataplatform.dw.service.datasource.dynamic.DynamicDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @author 阿里-马云 * @date 2021/6/18 11:52 */ @Configuration public class DataSourceConfig { @Value("${mybatis.type-aliases-package}") private String typeAliasesPackage; @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean(name = "primarySource") @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "dynamicDataSource") public DynamicDataSource dynamicDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 配置缺省的数据源 // 默认数据源配置 DefaultTargetDataSource dynamicDataSource.setDefaultTargetDataSource(dataSource()); Map<Object, Object> targetDataSources = new HashMap<>(); // 额外数据源配置 TargetDataSources targetDataSources.put("primarySource", dataSource()); dynamicDataSource.setTargetDataSources(targetDataSources); return dynamicDataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource()); // 设置mybatis的主配置文件 ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); // 设置别名包 sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage); // 手动配置mybatis的mapper.xml资源路径,如果单纯使用注解方式,不需要配置该行 sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocations)); return sqlSessionFactoryBean.getObject(); } }
C、AOP切面
package com.yunzhangfang.platform.dataplatform.dw.service.datasource.aspect; import com.yunzhangfang.platform.dataplatform.dw.service.datasource.annotation.TargetDataSource; import com.yunzhangfang.platform.dataplatform.dw.service.datasource.dynamic.DynamicDataSource; import com.yunzhangfang.platform.dataplatform.dw.service.datasource.holder.DynamicDataSourceContextHolder; import com.yunzhangfang.platform.dataplatform.dw.service.datasource.init.MultipleDatasourcesIniter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @author 阿里-马云 * @date 2021/6/18 9:47 */ @Aspect @Component @Slf4j public class DynamicDattaSourceAspect { @Autowired private MultipleDatasourcesIniter multipleDatasourcesIniter; @Autowired private DynamicDataSource dynamicDataSource; //改变数据源 @Before("@annotation(targetDataSource)") public void determineDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) { String dsName = targetDataSource.name(); if(StringUtils.isBlank(dsName)) { // 若数据源名称未配置,则走默认数据源 log.info("使用默认数据源"); } else { // 判断此数据源是否已被加载 if(!dynamicDataSource.getTargetDataSources().containsKey(dsName)) {
// init方法为从数据库中获取配置信息 dynamicDataSource.createDataSource(multipleDatasourcesIniter.init()); } DynamicDataSourceContextHolder.setDataSource(dsName); log.info("使用外部数据源,数据源为:{}", dsName); } } @After("@annotation(targetDataSource)") public void clearDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) { log.info("清除数据源 " + targetDataSource.name() + " !"); DynamicDataSourceContextHolder.clearDataSource(); } }
D、DynamicDataSourceContextHolder
package com.yunzhangfang.platform.dataplatform.dw.service.datasource.holder; /** * 动态数据源上下文管理 */ public class DynamicDataSourceContextHolder { /** * 存放当前线程使用的数据源类型信息 */ private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); /** * 设置数据源 * @param dataSource */ public static void setDataSource(String dataSource) { contextHolder.set(dataSource); } /** * 获取数据源 * @return */ public static String getDataSource() { return contextHolder.get(); } /** * 清除数据源 */ public static void clearDataSource() { contextHolder.remove(); } }
E、注解
package com.yunzhangfang.platform.dataplatform.dw.service.datasource.annotation; import java.lang.annotation.*; /** * 运行时生效,可作用于类以及方法上 * 规范:此注解只应用在repository上,一个repository只允许访问一个数据源 * 使用方法:将此注解标注在repository层,属性name标识数据源名称(dsName),dsName相关信息维护在表yzf_datasource_config */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TargetDataSource { String name() default ""; }
F、使用方法
默认数据源可以直接不配置,如果是其他数据源,需要在repository层手动标注数据源名称,如下:
@Component public class DictDataRepository { @Autowired private SysDictDataMapper dictDataMapper; /** * 根据字典类型查询字典数据 * * @param dictType * @return */ @TargetDataSource(name = "baUser") public List<SysDictData> queryDictDataByType(String dictType) { // operate db } }