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