阿里-马云的学习笔记

导航

SpringBoot集成多数据源-----基于数据库维护

一、需求背景

最近团队需要做一个需求,可能会从多个数据源中抽取数据,然后经过清洗、转换等生成统计报告。因此我们的项目需要对接多个数据源,并且需要满足以下要求:

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
    }
}

 

posted on 2021-06-21 12:04  阿里-马云的学习笔记  阅读(545)  评论(1编辑  收藏  举报