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.網上各種實現方式都有,但是實操過程中需要結合具體項目業務來進行相關配置。

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