公司前一阵子有一个业务需求,需要从公司其它系统的数据库获取数据。原定方案直接连接数据库获取数据,但是最后改成他们给我提供接口,我们调用接口获取数据。因为有这业务需求,我才开始思考一个系统如何连接多个数据库的问题,我自己利用业务时间,经过查阅资料,通过以下方式实现多数据源的切换,具体如下:
创建一个类来继承AbstractRoutingDataSource抽象类,并实现determineCurrentLookupKey()方法,完成多数据源动态切换的功能。连接数据库时determineCurrentLookupKey()方法会返回的数据,将作为key去存储多个数据源Map中查找相应的值,连接相应的数据源。
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
}
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
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();
}
}
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");
}
}
}
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;
}
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();
}
}
###手动配置多数据源###
#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
@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);
}
}