SpringBoot2.X版本之Mybatis多数据源动态切换

前言:

 

公司前一阵子有一个业务需求,需要从公司其它系统的数据库获取数据。原定方案直接连接数据库获取数据,但是最后改成他们给我提供接口,我们调用接口获取数据。因为有这业务需求,我才开始思考一个系统如何连接多个数据库的问题,我自己利用业务时间,经过查阅资料,通过以下方式实现多数据源的切换,具体如下:

Mybatis多数据源动态切换核心原理:

创建一个类来继承AbstractRoutingDataSource抽象类,并实现determineCurrentLookupKey()方法,完成多数据源动态切换的功能。连接数据库时determineCurrentLookupKey()方法会返回的数据,将作为key去存储多个数据源Map中查找相应的值,连接相应的数据源。

代码:

1.DataSourceKey(数据源名称枚举类)

package com.hanxiaozhang.config.dataSource;

/**
 * 〈一句话功能简述〉
* 〈数据源名称枚举类〉 * * @author hanxinghua * @create 2019/8/12 * @since 1.0.0 */ public enum DataSourceKey { /* 主数据库源 */ DB_MASTER, /* 从数据库源1 */ DB_SLAVE1, /* 从数据库源2 */ DB_SLAVE2, /* 其他数据库源 */ DB_OTHER }

2.DynamicDataSourceConfig(动态切换数据源配置类)

package com.hanxiaozhang.config.dataSource;


import com.alibaba.druid.pool.DruidDataSource;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;


import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 〈一句话功能简述〉
* 〈动态切换数据源配置类〉 * * @author hanxinghua * @create 2019/8/12 * @since 1.0.0 */ @Slf4j @MapperScan(basePackages = "com.hanxiaozhang.*.dao") @Configuration public class DynamicDataSourceConfig { @Bean @Primary @ConfigurationProperties(prefix = "datasource.master") public DataSource dbMaster() { log.info("Initialized -> [{}]", "DataSource DB_Master Start"); return new DruidDataSource(); } @Bean @ConfigurationProperties(prefix = "datasource.slave1") public DataSource dbSlave1() { log.info("Initialized -> [{}]", "DataSource DB_Slave1 Start"); return new DruidDataSource(); } @Bean @ConfigurationProperties(prefix = "datasource.slave2") public DataSource dbSlave2() { log.info("Initialized -> [{}]", "DataSource DB_Slave2 Start"); return new DruidDataSource(); } @Bean @ConfigurationProperties(prefix = "datasource.other") public DataSource dbOther() { log.info("Initialized -> [{}]", "DataSource DB_Other Start"); return new DruidDataSource(); } /** * 核心动态数据源 * * @return 数据源实例 */ @Bean public DataSource dynamicDataSource() { log.info("Initialized -> [{}]", "DynamicDataSource Start"); DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); //配置默认目标数据源 dataSource.setDefaultTargetDataSource(dbMaster()); Map dataSourceMap = new HashMap<>(4); dataSourceMap.put(DataSourceKey.DB_MASTER, dbMaster()); dataSourceMap.put(DataSourceKey.DB_SLAVE1, dbSlave1()); dataSourceMap.put(DataSourceKey.DB_SLAVE2, dbSlave2()); dataSourceMap.put(DataSourceKey.DB_OTHER, dbOther()); //配置目标数据源Map dataSource.setTargetDataSources(dataSourceMap); return dataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { log.info("Initialized -> [{}]", "SqlSessionFactory Start"); SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); //配置数据源 sqlSessionFactoryBean.setDataSource(dynamicDataSource()); //解决找不到mapper文件的问题 sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/**/*Mapper.xml")); org.apache.ibatis.session.Configuration configuration =new org.apache.ibatis.session.Configuration(); //配置驼峰映射 configuration.setMapUnderscoreToCamelCase(true); //配置打印SQL日志,yml中logging以配置打印com.hanxiaozhang包下所有日志 //configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class); sqlSessionFactoryBean.setConfiguration(configuration); //配置扫描通配符包的路径,可以配置自动扫描包路径给类配置别名 sqlSessionFactoryBean.setTypeAliasesPackage("com.hanxiaozhang.**.domain"); return sqlSessionFactoryBean.getObject(); } @Bean public SqlSessionTemplate sqlSessionTemplate() throws Exception { log.info("Initialized -> [{}]", "SqlSessionTemplate Start"); return new SqlSessionTemplate(sqlSessionFactory()); } /** * 事务管理 * * @return 事务管理实例 */ @Bean public PlatformTransactionManager platformTransactionManager() { log.info("Initialized -> [{}]", "PlatformTransactionManager Start"); return new DataSourceTransactionManager(dynamicDataSource()); } }

 3.DynamicRoutingDataSource(动态创建数据源)

package com.hanxiaozhang.config.dataSource;

import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;


/**
 * 〈一句话功能简述〉
* 〈动态创建数据源〉 * * @author hanxinghua * @create 2019/8/12 * @since 1.0.0 */ @Slf4j public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { if(null==DynamicDataSourceContextHolder.get()){ log.info("Current use DataSource to:[{}]","Default DataSource"); }else { log.info("Current use DataSource to:[{}]",DynamicDataSourceContextHolder.get()); } return DynamicDataSourceContextHolder.get(); } }

4.DynamicDataSourceContextHolder(数据源上下文配置,用于切换数据源)

package com.hanxiaozhang.config.dataSource;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;

/**
 * 〈一句话功能简述〉
* 〈数据源上下文配置,用于切换数据源〉 * Tips:ThreadLocal 解决线程安全问题,多线程程序的并发 * * @author hanxinghua * @create 2019/8/12 * @since 1.0.0 */ @Slf4j public class DynamicDataSourceContextHolder { private static final ThreadLocal currentDatesource = new ThreadLocal<>(); /** * 清除当前数据源 */ public static void clear() { currentDatesource.remove(); } /** * 获取当前使用的数据源 * * @return 当前使用数据源的ID */ public static DataSourceKey get() { return currentDatesource.get(); } /** * 设置当前使用的数据源 * * @param value 需要设置的数据源ID */ public static void set(DataSourceKey value) { currentDatesource.set(value); } /** * 设置从从库读取数据 * Tips:没有配置,直接读DB_SLAVE1库 2019-08-13 * */ public static void setSlave() { if (RandomUtils.nextInt(0, 2) > 0) { DynamicDataSourceContextHolder.set(DataSourceKey.DB_SLAVE2); log.info("Random Use DataSource to:[{}]", "DB_Slave2"); } else { DynamicDataSourceContextHolder.set(DataSourceKey.DB_SLAVE1); log.info("Random Use DataSource to:[{}]", "DB_Slave1"); } } }

5.TargetDataSource(定义目标数据源注解类)

package com.hanxiaozhang.annotation;

import com.hanxiaozhang.config.dataSource.DataSourceKey;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 〈一句话功能简述〉
* 〈定义目标数据源注解类〉 * * @author hanxinghua * @create 2019/8/12 * @since 1.0.0 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TargetDataSource { DataSourceKey dataSourceKey() default DataSourceKey.DB_MASTER; }

6.DynamicDataSourceAspect(动态切换切换数据源切面)

package com.hanxiaozhang.config.dataSource;

import com.hanxiaozhang.annotation.TargetDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.log4j.Logger;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.hanxiaozhang.config.dataSource.DataSourceKey;

import java.lang.reflect.Method;

/**
 * 〈一句话功能简述〉
* 〈动态切换切换数据源切面〉 * * @author hanxinghua * @create 2019/8/12 * @since 1.0.0 */ @Slf4j @Aspect @Order(-1) @Component public class DynamicDataSourceAspect { private static final Logger LOG = Logger.getLogger(DynamicDataSourceAspect.class); @Pointcut("(execution(* com.hanxiaozhang..service.*.list*(..)))||" + "(execution(* com.hanxiaozhang..service.*.count*(..)))||"+ "(execution(* com.hanxiaozhang..service.*.select*(..)))||"+ "(execution(* com.hanxiaozhang..service.*.get*(..)))") public void pointCut() { } @Before("pointCut()") public void doBeforeWithSlave(JoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); log.info("Execute DataSource AOP Method:{}",methodSignature.getDeclaringTypeName() + "." + methodSignature.getName()); //获取当前切点方法对象 Method method = methodSignature.getMethod(); //判断是否为接口方法 if (method.getDeclaringClass().isInterface()) { try { //获取实际类型的方法对象 method = joinPoint.getTarget().getClass() .getDeclaredMethod(joinPoint.getSignature().getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { LOG.error("方法不存在!", e); } } //判断该方法是否存在TargetDataSource注解 if (null == method.getAnnotation(TargetDataSource.class)) { DynamicDataSourceContextHolder.setSlave(); } } @After("pointCut()") public void after(JoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); log.info("Clear DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.get(), methodSignature.getDeclaringTypeName() + "." + methodSignature.getName()); DynamicDataSourceContextHolder.clear(); } /** * 执行方法前更换数据源 * * @param joinPoint 切点 * @param targetDataSource 动态数据源 */ @Before("@annotation(targetDataSource)") public void doBefore(JoinPoint joinPoint, TargetDataSource targetDataSource) { DataSourceKey dataSourceKey = targetDataSource.dataSourceKey(); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); if (dataSourceKey != DataSourceKey.DB_MASTER) { log.info("Set DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.get(), methodSignature.getDeclaringTypeName() + "." + methodSignature.getName()); DynamicDataSourceContextHolder.set(dataSourceKey); } else { log.info("Use Default DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.get(), methodSignature.getDeclaringTypeName() + "." + methodSignature.getName()); DynamicDataSourceContextHolder.set(DataSourceKey.DB_MASTER); } } /** * 执行方法后清除数据源设置 * * @param joinPoint 切点 * @param targetDataSource 动态数据源 */ @After("@annotation(targetDataSource)") public void doAfter(JoinPoint joinPoint, TargetDataSource targetDataSource) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); log.info("Clear DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.get(), methodSignature.getDeclaringTypeName() + "." + methodSignature.getName()); DynamicDataSourceContextHolder.clear(); } }

 7.YAML文件

###手动配置多数据源###
#master
datasource:
  master:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3307}/${DATABASE_NAME:bootdo}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8
    username: ${MYSQL_USER:root}
    password: ${MYSQL_PASS:root}
    initialSize: ${global.druid.initial-size}
    minIdle: ${global.druid.minIdle}
    maxActive: ${global.druid.maxActive}
    # 配置获取连接等待超时的时间
    maxWait: ${global.druid.maxWait}
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: ${global.druid.timeBetweenEvictionRunsMillis}
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    minEvictableIdleTimeMillis: ${global.druid.minEvictableIdleTimeMillis}
    validationQuery: ${global.druid.validationQuery}
    testWhileIdle: ${global.druid.testWhileIdle}
    testOnBorrow: ${global.druid.testOnBorrow}
    testOnReturn: ${global.druid.testOnReturn}
    # 打开PSCache,并且指定每个连接上PSCache的大小
    poolPreparedStatements: ${global.druid.poolPreparedStatements}
    maxPoolPreparedStatementPerConnectionSize: ${global.druid.maxPoolPreparedStatementPerConnectionSize}
    # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: ${global.druid.filters}
    # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
    connectionProperties: ${global.druid.connectionProperties}
    # 合并多个DruidDataSource的监控数据
    #useGlobalDataSourceStat: ${global.druid.useGlobalDataSourceStat}
#slave1
  slave1:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://${SLAVE1_MYSQL_HOST:127.0.0.1}:${SLAVE1_MYSQL_PORT:3306}/${SLAVE1_DATABASE_NAME:bootdo}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8
    username: ${SLAVE1_MYSQL_USER:root}
    password: ${SLAVE1_MYSQL_PASS:root}
    initialSize: ${global.druid.initial-size}
    minIdle: ${global.druid.minIdle}
    maxActive: ${global.druid.maxActive}
    maxWait: ${global.druid.maxWait}
    timeBetweenEvictionRunsMillis: ${global.druid.timeBetweenEvictionRunsMillis}
    minEvictableIdleTimeMillis: ${global.druid.minEvictableIdleTimeMillis}
    validationQuery: ${global.druid.validationQuery}
    testWhileIdle: ${global.druid.testWhileIdle}
    testOnBorrow: ${global.druid.testOnBorrow}
    testOnReturn: ${global.druid.testOnReturn}
    poolPreparedStatements: ${global.druid.poolPreparedStatements}
    maxPoolPreparedStatementPerConnectionSize: ${global.druid.maxPoolPreparedStatementPerConnectionSize}
    filters: ${global.druid.filters}
    connectionProperties: ${global.druid.connectionProperties}
    #useGlobalDataSourceStat: ${global.druid.useGlobalDataSourceStat}

#slave2
  slave2:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://${SLAVE2_MYSQL_HOST:127.0.0.1}:${SLAVE2_MYSQL_PORT:3306}/${SLAVE2_DATABASE_NAME:bootdo}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&allowMultiQueries=true
    username: ${SLAVE2_MYSQL_USER:root}
    password: ${SLAVE2_MYSQL_PASS:root}
    initialSize: ${global.druid.initial-size}
    minIdle: ${global.druid.minIdle}
    maxActive: ${global.druid.maxActive}
    maxWait: ${global.druid.maxWait}
    timeBetweenEvictionRunsMillis: ${global.druid.timeBetweenEvictionRunsMillis}
    minEvictableIdleTimeMillis: ${global.druid.minEvictableIdleTimeMillis}
    validationQuery: ${global.druid.validationQuery}
    testWhileIdle: ${global.druid.testWhileIdle}
    testOnBorrow: ${global.druid.testOnBorrow}
    testOnReturn: ${global.druid.testOnReturn}
    poolPreparedStatements: ${global.druid.poolPreparedStatements}
    maxPoolPreparedStatementPerConnectionSize: ${global.druid.maxPoolPreparedStatementPerConnectionSize}
    filters: ${global.druid.filters}
    connectionProperties: ${global.druid.connectionProperties}
    #useGlobalDataSourceStat: ${global.druid.useGlobalDataSourceStat}
#other
  other:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:${OTHER_MYSQL_HOST:127.0.0.1}:${OTHER_MYSQL_PORT:3306}/${OTHER_DATABASE_NAME:bootdo}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&allowMultiQueries=true
    username: ${OTHER_MYSQL_USER:root}
    password: ${OTHER_MYSQL_PASS:root}
    initialSize: ${global.druid.initial-size}
    minIdle: ${global.druid.minIdle}
    maxActive: ${global.druid.maxActive}
    maxWait: ${global.druid.maxWait}
    timeBetweenEvictionRunsMillis: ${global.druid.timeBetweenEvictionRunsMillis}
    minEvictableIdleTimeMillis: ${global.druid.minEvictableIdleTimeMillis}
    validationQuery: ${global.druid.validationQuery}
    testWhileIdle: ${global.druid.testWhileIdle}
    testOnBorrow: ${global.druid.testOnBorrow}
    testOnReturn: ${global.druid.testOnReturn}
    poolPreparedStatements: ${global.druid.poolPreparedStatements}
    maxPoolPreparedStatementPerConnectionSize: ${global.druid.maxPoolPreparedStatementPerConnectionSize}
    filters: ${global.druid.filters}
    connectionProperties: ${global.druid.connectionProperties}
    #useGlobalDataSourceStat: ${global.druid.useGlobalDataSourceStat}


###公共druid配置数据###
global.druid:
  initialSize: 1
  minIdle: 3
  maxActive: 20
  # 配置获取连接等待超时的时间
  maxWait: 60000
  # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  timeBetweenEvictionRunsMillis: 60000
  # 配置一个连接在池中最小生存的时间,单位是毫秒
  minEvictableIdleTimeMillis: 30000
  validationQuery: select 'x'
  testWhileIdle: true
  testOnBorrow: false
  testOnReturn: false
  # 打开PSCache,并且指定每个连接上PSCache的大小
  poolPreparedStatements: true
  maxPoolPreparedStatementPerConnectionSize: 20
  # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  filters: stat,wall,slf4j
  # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
  connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  # 合并多个DruidDataSource的监控数据
  useGlobalDataSourceStat: true

8.取消自动配置数据源

@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class,
        org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
public class HanxiaozhangApplication {

    public static void main(String[] args) {
        SpringApplication.run(HanxiaozhangApplication.class, args);
    }

}

使用: 

1.使用注解方式:

SpringBoot2.X版本之Mybatis多数据源动态切换_第1张图片

2.默认方式,以list*、count*、select*、get*开通的方法轮询DB_SLAVE1和DB_SLAVE2数据源;其他方法默认DB_MASTER

3.源码可以通过邮箱获取:[email protected]

你可能感兴趣的:(java,#Springboot,Mybatis,spring,spring,boot,数据库)