springboot 多数据源实现

在springboot项目中多数据源的实现得益于spring-jdbc的高版本提供的AbstractRoutingDataSource抽象类,让原来在spring+mybatis项目中只能通过繁琐复杂的对每个Mapper单独配置配置sqlsession的实现变得更加简单。

一、实现

1.多数据源配置文件


spring.datasource.hikari.master.name = master
spring.datasource.hikari.master.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.hikari.master.jdbc-url = jdbc:mysql://xxxxxxx:3306/d_enterprise?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.hikari.master.port = 3306
spring.datasource.hikari.master.username = zhousi
spring.datasource.hikari.master.password = xxxxxx@123

# SlaveAlpha datasource config
spring.datasource.hikari.slave-ehr.name = oaEhrDb
spring.datasource.hikari.slave-ehr.driver-class-name = com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.hikari.slave-ehr.jdbc-url = jdbc:sqlserver://xxxx.xx.xx:1433;database=XXX
spring.datasource.hikari.slave-ehr.port = 3306
spring.datasource.hikari.slave-ehr.username = xxx
spring.datasource.hikari.slave-ehr.password = xxxxxxxPWD

2.管理多数据源的AbstractRoutingDataSource的子类

AbstractRoutingDataSource的内部维护了一个名为targetDataSources的Map,并提供的setter方法用于设置数据源关键字与数据源的关系,实现类被要求实现其determineCurrentLookupKey()方法,由此方法的返回值决定具体从哪个数据源中获取连接。


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 多数据源动态设置
 *
 * @author zhousi
 * @date 2020/5/28
 */

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Set dynamic DataSource to Application Context
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        logger.debug("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey());
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}
3.枚举维护多数据源名称
public enum DataSourceKeyEnum {

    master,

    oaEhr
}
4.动态切换数据源的静态线程
public class DynamicDataSourceContextHolder {

    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

    private static int counter = 0;

    /**
     * 数据源名线程
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = ThreadLocal.withInitial(DataSourceKeyEnum.master::name);


    /**
     * 数据源 List
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * The constant slaveDataSourceKeys.
     */
    public static List<Object> slaveDataSourceKeys = new ArrayList<>();

    /**
     * To switch DataSource
     *
     * @param key the key
     */
    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }

    /**
     * Use master data source.
     */
    public static void useMasterDataSource() {
        CONTEXT_HOLDER.set(DataSourceKeyEnum.master.name());
    }

    /**
     * Use slave data source.
     */
    public static void useSlaveDataSource() {
        try {
            int datasourceKeyIndex = counter % slaveDataSourceKeys.size();
            CONTEXT_HOLDER.set(String.valueOf(slaveDataSourceKeys.get(datasourceKeyIndex)));
            counter++;
        } catch (Exception e) {
            logger.error("Switch slave datasource failed, error message is {}", e.getMessage());
            useMasterDataSource();
            e.printStackTrace();
        }
    }

    /**
     * Get current DataSource
     *
     * @return data source key
     */
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * To set DataSource as default
     */
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }

    /**
     * Check if give DataSource is in current DataSource list
     *
     * @param key the key
     * @return boolean boolean
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }
5.数据源装配到Springboot容器
@Configuration
public class DataSourceConfigurer {

    @Bean("master")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.hikari.master")
    public DataSource master() {
        return DataSourceBuilder.create().build();
    }


    @Bean("oaEhrDb")
    @ConfigurationProperties(prefix = "spring.datasource.hikari.slave-ehr")
    public DataSource oaEhrDb() {
        return DataSourceBuilder.create().build();
    }

    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>(4);
        dataSourceMap.put(DataSourceKeyEnum.master.name(), master());
        //其他数据源
        dataSourceMap.put(DataSourceKeyEnum.oaEhr.name(), oaEhrDb());

        // 默认数据源(主数据源)
        dynamicRoutingDataSource.setDefaultTargetDataSource(master());
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
		
		//所数据源放入TargetDataSourceMap
        DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
        DynamicDataSourceContextHolder.slaveDataSourceKeys.addAll(dataSourceMap.keySet());
        DynamicDataSourceContextHolder.slaveDataSourceKeys.remove(DataSourceKeyEnum.master.name());
        return dynamicRoutingDataSource;
    }

    /**
     * 必须重构SqlSessionFactoryBean
     * mybatis提供的sqlsessionFactory默认是单数据源,不重构会一直只能操作主数据源,无法切换
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // mybatis的mapper扫描设置
		//此属性在找不到mapper文件的时候使用次设置
		//sqlSessionFactoryBean.setTypeAliasesPackage("com.mbcloud.enterprise.user.base.mapper");
		//此属性在找不到Resource时候必须设置此属性(路径表达式必须正确,否则启动报错)
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
        //重要:::必须配置sqlSessionFactory的多数据源,否则多数据源不生效
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        return sqlSessionFactoryBean;
    }

    /**
     * 多数据源 事务配置
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
6.使用切面在执行sql前切换数据源

使用切面来实现业务层的动态数据源切换可以非常灵活,根据我实际使用给出两个不同切点的实现方式
方式一:切点在mapper的dao层

package com.mbcloud.enterprise.thirddata.core.config.datasource;

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.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 钱换数据源切面
 *
 * @author zhousi
 * @date 2020/5/28
 */
@Aspect
@Component
public class DynamicDataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    private final String[] QUERY_PREFIX = {"getOA"};

    /**
     * Dao aspect.
     */
    @Pointcut("execution( * com.mbcloud.enterprise.thirddata.base.mapper.*.*(..))")
    public void daoAspect() {
    }

    /**
     * Switch DataSource
     *
     * @param point the point
     */
    @Before("daoAspect()")
    public void switchDataSource(JoinPoint point) {
        Boolean isQueryMethod = isQueryMethod(point.getSignature().getName());
        if (isQueryMethod) {
            DynamicDataSourceContextHolder.useSlaveDataSource();
            logger.debug("Switch DataSource to [{}] in Method [{}]",
                    DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
        }
    }

    /**
     * Restore DataSource
     *
     * @param point the point
     */
    @After("daoAspect()")
    public void restoreDataSource(JoinPoint point) {
        DynamicDataSourceContextHolder.clearDataSourceKey();
        logger.debug("Restore DataSource to [{}] in Method [{}]",
                DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
    }


    /**
     * Judge if method start with query prefix
     *
     * @param methodName
     * @return
     */
    private Boolean isQueryMethod(String methodName) {
        for (String prefix : QUERY_PREFIX) {
            if (methodName.startsWith(prefix)) {
                return true;
            }
        }
        return false;
    }

}

方式二:切点在自定义注解上
(1)自定义注解

package com.mbcloud.enterprise.thirddata.core.config.datasource;

import java.lang.annotation.*;

/**
 * 数据源注解
 * @author zhousi
 * @date 2020/5/28
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    //数据源名
    String name();
}

(2)切面

package com.mbcloud.enterprise.thirddata.core.config.datasource;

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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 钱换数据源切面
 *
 * @author zhousi
 * @date 2020/5/28
 */
@Aspect
@Component
public class DynamicDataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);


    /**
     * Switch DataSource
     *
     * @param point the point
     */
    @Before("@annotation(dataSource)")
    public void switchDataSource(JoinPoint point, TargetDataSource dataSource) {
        String dsName = dataSource.name();
        if (DynamicDataSourceContextHolder.slaveDataSourceKeys.contains(dsName)) {
            DynamicDataSourceContextHolder.useSlaveDataSource();
            logger.debug("Switch DataSource to [{}] in Method [{}]", dsName);
            DynamicDataSourceContextHolder.setDataSourceKey(dsName);
        }
    }

    /**
     * Restore DataSource
     *
     * @param point the point
     */
    @After("@annotation(dataSource)")
    public void restoreDataSource(JoinPoint point, TargetDataSource dataSource) {
        DynamicDataSourceContextHolder.clearDataSourceKey();
        logger.debug("Restore DataSource to [{}] in Method [{}]",
                DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
    }


}

两种方式,第一种方式更简单快捷,但是需要时时控制dao层的方法命名,不够灵活;方式二更加便捷的灵活,使用能明确指定数据源使得编码的可读性好,推荐使用方式二。

7.事务管理

多数据源导致@Transaction的事务管理失效,解决方式有管理两个selSessionFactory、重构jdbcTemplate等,较复杂,为了简单快捷可以使用编程式事务来快捷实现
方式一:手动事务

 @Autowired
    private TransactionTemplate transactionTemplate;
 @TargetDataSource(name = DataSourceName.MASTER_DB)
    public void initDalyOrgData(List<OvwDep> allDeps) {
        List<Org> orgInsertList = BeanConvertUtil.makeOrgListFromDOList(allDeps);
        if (CollectionUtils.isNotEmpty(orgInsertList)) {
            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    orgMapper.deleteYesterdayOrg();
                    orgMapper.batchInsertOrg(orgInsertList);
                }
            });

        }
    }

其他方式待补充…

二、参考博文

1.参考网上的springboot的连接池多数据源实操起来会有非常多的问题,找到一篇博客写的比较全,有错误分析和源码,参考的博文地址
2.网上各种实现方式都有,但是实操过程中需要结合具体项目业务来进行相关配置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章