在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.网上各种实现方式都有,但是实操过程中需要结合具体项目业务来进行相关配置。