2017年8月16日11:45:39更新:这里贴出我自己写的一个demo,放在CSDN上了。http://download.csdn.net/download/llzhaoyun/9912658
前面写了一篇关于使用Spring + druid + Mybatis配置多数据源,并在代码中动态选择使用不同的数据源的博文,当时写该文时,由于比较忙,只简单做了记录,后来又做了一些扩展,添加了分布式事务等,今天就另起一文,详细记录下目前生产线上正在使用的稳定版本。
业务场景:
有一个系统A作为数据中转服务器。若干个系统数据都需要经过系统A做处理、转发,而数据交互有多种方式,如WebService、REST API、中间表等,A也会操作自己的数据库。本文主要围绕操作中间表的方式来讲。
A需要操作的自己的数据库是确定的,但由于接入的系统可能随业务发展而增加或改变,所以不能将中间表的连接信息写死,需要使用配置文件维护,并在服务器启动的时候动态加载,然后由不同的业务去决定使用哪一个数据源。(该段文本描述的业务问题已在上篇文章https://my.oschina.net/simpleton/blog/868608中解决)
由于系统A会读中间表,然后将读取的数据预处理,然后存入A系统的数据库或其他中间库,如果使用DataSourceTransactionManager作为事务管理器来控制事务,会出现多数据库事务不一致的问题,那么,解决这类问题有两种方案:编程式事务和分布式事务。如果使用编程式事务的话,需要在每个地方显示地管理实务(开启、提交、回滚),耦合较高。对于复杂的业务来说,不好处理,故不适合系统A的业务场景。后来看了分布式事务框架atomikos,觉得应该能解决我目前的问题,经测试,效果很赞,下面就让我们一起来看看它是什么集成到系统的。(原理的话,大家自己百度一下)
好了,说了这么多,下面来一步一步解决我们问题。
一、配置2套数据源(defaultDataSource:管理A系统自己数据库连接的数据源。atomikosDynamicDataSource:管理数据源动态初始化、选择、销毁的数据源)。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
">
<!-- 默认的dataSource -->
<bean id="defaultDataSource" class="com.atomikos.jdbc.AtomikosDataSourceBean" destroy-method="close">
<!-- uniqueResourceName很重要,不能重复 -->
<property name="uniqueResourceName" value="defaultDataSource"/>
<property name="xaDataSourceClassName" value="com.alibaba.druid.pool.xa.DruidXADataSource" />
<property name="borrowConnectionTimeout" value="${pro.borrowConnectionTimeout}" />
<property name="minPoolSize" value="${pro.minPoolSize}" />
<property name="maxPoolSize" value="${pro.maxPoolSize}" />
<property name="maxLifetime" value="0" />
<!-- xaProperties实质上是设置属性xaDataSourceClassName的对象的值,即com.alibaba.druid.pool.xa.DruidXADataSource的参数值 -->
<property name="xaProperties">
<props>
<prop key="driverClassName">${pro.driver}</prop>
<prop key="url">${pro.url}</prop>
<prop key="username">${pro.username}</prop>
<prop key="password">${pro.password}</prop>
<prop key="initialSize">${pro.initialSize}</prop>
<prop key="minIdle">${pro.minIdle}</prop>
<prop key="maxActive">${pro.maxActive}</prop>
<prop key="maxWait">${pro.maxWait}</prop>
<prop key="timeBetweenEvictionRunsMillis">${pro.timeBetweenEvictionRunsMillis}</prop>
<prop key="minEvictableIdleTimeMillis">${pro.minEvictableIdleTimeMillis}</prop>
<prop key="validationQuery">SELECT 1</prop>
<prop key="testWhileIdle">true</prop>
<prop key="testOnBorrow">false</prop>
<prop key="testOnReturn">false</prop>
<prop key="poolPreparedStatements">true</prop>
<prop key="maxPoolPreparedStatementPerConnectionSize">${pro.maxPoolPreparedStatementPerConnectionSize}</prop>
<prop key="filters">stat</prop>
</props>
</property>
</bean>
<!-- 定义主业务使用的sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation">
<value>classpath:config/sqlMapConfig.xml</value>
</property>
<property name="dataSource" ref="defaultDataSource" />
<property name="typeAliasesPackage" value="com.eya.model.domain" />
<!-- 扫描defaultDataSource对应的数据源的SQL配置文件,这部分数据库操作就不需要动态切换数据源,直接使用默认的数据源操作数据库即可 -->
<property name="mapperLocations" value="classpath:com/eya/dao/**/*.xml" />
</bean>
<!-- 扫描mybatis的接口所在的文件 -->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.eya.dao,com.eya.pubmapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 定义中间表使用的数据源、sqlSessionFactory等 -->
<bean id="atomikosDynamicDataSource" class="com.eya.pubservice.datasource.AtomikosDynamicDataSource" >
<!-- <property name="defaultTargetDataSource" ref="defaultDataSource"></property> -->
<property name="targetDataSources">
<map>
<!-- 这里还可以加多个数据源,由于不确定数据源的地址信息,所以这里不配置,在代码中去动态初始化 -->
</map>
</property>
</bean>
<!-- 定义数据交互业务使用的sqlSessionFactory -->
<bean id="sqlSessionFactory4MiddleTable" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="atomikosDynamicDataSource" />
<!-- 扫描中间表的SQL配置文件 -->
<property name="mapperLocations" value="classpath:com/eya/middletable/**/*.xml" />
</bean>
<!-- 扫描mybatis的接口所在的文件,注意这里扫描的mybatis的xml文件和主业务的xml文件不同 -->
<bean id="mapperScannerConfigurer4MiddleTable" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.eya.middletable" />
<!-- 这里要使用sqlSessionFactoryBeanName属性,否则会报错 -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory4MiddleTable"/>
</bean>
<!-- jta分布式事务 -->
<!-- Atomikos 事务管理器配置 -->
<bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager"
init-method="init" destroy-method="close">
<!-- <property name="startupTransactionService" value="false" /> -->
<!-- close()时是否强制终止事务 -->
<property name="forceShutdown" value="false" />
</bean>
<!-- Atomikos UserTransaction配置 -->
<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp"></bean>
<!-- JTA事务管理器 -->
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager">
<ref bean="atomikosTransactionManager" />
</property>
<property name="userTransaction">
<ref bean="atomikosUserTransaction" />
</property>
</bean>
<!-- 开启注解事务,使用@Transactional来标记需要事务控制的方法或类 -->
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true" />
</beans>
说明一下,这是spring的配置文件,主要配置mybatis的信息,其中涉及到了数据源、mybatis的SQL文件和接口类、声明事务管理。这里面有几个需要重点指出的地方
1.文件中的表达式,如${pro.url},是引入properties的内容。
2.DataSource的定义:和原来定义DataSource不同,原来定义时,是使用class为com.alibaba.druid.pool.xa.DruidXADataSource(也可以是c3p0)来定义bean,然后配置数据源的信息,现在是定义class为com.atomikos.jdbc.AtomikosDataSourceBean的bean,然后使用xaDataSourceClassName指明具体使用的数据源类,使用xaProperties来配置数据源的属性。
3.defaultDataSource:defaultDataSource为系统A自己的数据库信息,这个是确定的,所以直接在配置文件中定义好,然后根据defaultDataSource来配置sqlSessionFactory,并使用sqlSessionFactory来配置mapperScannerConfigurer,这部分是完成系统A自己的数据源配置以及myBatis配置(SQL文件扫描和接口类)。注意:定义org.mybatis.spring.mapper.MapperScannerConfigurer的bean时,需要使用sqlSessionFactoryBeanName属性来指定使用哪一个sqlSessionFactory,不然会抛出异常。
4.atomikosDynamicDataSource:这是一个自己实现的数据源类(代码我稍后贴出来并讲解代码中的内容),属性targetDataSources是一个map集合,用于存储不同的数据源信息。以上配置文件中,没有配置map的值,因为不能像defaultDataSource一样确定数据库连接地址等信息,所以在服务器启动的时候,用代码去初始化不同的数据源信息:根据配置的多个数据库信息,初始化对应个数的数据源,然后存放到targetDataSources中。targetDataSources被初始化后,在系统运行过程中,根据不同的业务,从targetDataSources中选择不同的数据源来操作数据库。
5.分布式事务配置:在配置文件中定义了atomikosTransactionManager、atomikosUserTransaction两个bean,然后在txManager中注入这2个bean,并在最后使用<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true" />开启注解事务。使用过程中,出现了事务提交超时的问题(com.atomikos.icatch.RollbackException: Prepare: NO vote),解决方案是添加一个atomikos配置文件,配置事务的相关属性,具体内容参考我的博文:https://my.oschina.net/simpleton/blog/915683
说明:大部分情况下,按照上述配置,在配置 atomikosDynamicDataSource的时候,同时配置targetDataSources的值,并在代码中动态确定使用哪套数据源(继承AbstractRoutingDataSource并实现determineCurrentLookupKey即可)就可以了,这也是网上目前大部分资料已经完成的内容。
二、创建AbstractDynamicDataSource、AtomikosDynamicDataSource,管理动态创建的数据源,然后创建一个标记使用的数据源的上下文对象:DBContextHolder,用来标记当前业务需要使用哪一个数据源。
package com.eya.pubservice.datasource;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 管理动态数据源的数据源父类
* @create ll
* @createDate 2017年3月27日 下午2:38:05
* @update
* @updateDate
*/
public abstract class AbstractDynamicDataSource<T extends DataSource> extends
AbstractRoutingDataSource
implements
ApplicationContextAware {
/** 日志 */
protected Logger logger = LoggerFactory.getLogger(getClass());
/** 默认的数据源KEY */
//protected static final String DEFAULT_DATASOURCE_KEY = "defaultDataSource";
/** 数据源KEY-VALUE键值对 */
private Map<Object, Object> targetDataSources;
/** spring容器上下文 */
private static ApplicationContext ctx;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ctx = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return ctx;
}
public static Object getBean(String name) {
return ctx.getBean(name);
}
/**
* @param targetDataSources the targetDataSources to set
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
super.setTargetDataSources(targetDataSources);
// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
super.afterPropertiesSet();
}
/**
* @return the targetDataSources
*/
public Map<Object, Object> getTargetDataSources() {
return this.targetDataSources;
}
/**
* 创建数据源
* @param driverClassName 数据库驱动名称
* @param url 连接地址
* @param username 用户名
* @param password 密码
* @return 数据源{@link T}
* @Author : ll. create at 2017年3月27日 下午2:44:34
*/
public abstract T createDataSource(String driverClassName, String url, String username,
String password);
/**
* 设置系统当前使用的数据源
* <p>数据源为空或者为0时,自动切换至默认数据源,即在配置文件中定义的默认数据源
* @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
*/
@Override
protected Object determineCurrentLookupKey() {
logger.debug("【设置系统当前使用的数据源】");
Map<String, Object> configMap = DBContextHolder.getDBType();
logger.debug("【当前数据源配置为:{}】", configMap);
if (MapUtils.isEmpty(configMap)) {
// 使用默认数据源
//return DEFAULT_DATASOURCE_KEY;
throw new IllegalArgumentException("没有指定数据源");
}
// 判断数据源是否需要初始化
this.verifyAndInitDataSource();
logger.debug("【切换至数据源:{}】", configMap);
return configMap.get(DBContextHolder.DATASOURCE_KEY);
}
/**
* 判断数据源是否需要初始化
* @Author : ll. create at 2017年3月27日 下午3:57:43
*/
private void verifyAndInitDataSource() {
Map<String, Object> configMap = DBContextHolder.getDBType();
Object obj = this.targetDataSources.get(configMap.get(DBContextHolder.DATASOURCE_KEY));
if (obj != null) {
return;
}
logger.info("【初始化数据源】");
T datasource = this.createDataSource(configMap.get(DBContextHolder.DATASOURCE_DRIVER)
.toString(), configMap.get(DBContextHolder.DATASOURCE_URL).toString(),
configMap.get(DBContextHolder.DATASOURCE_USERNAME).toString(),
configMap.get(DBContextHolder.DATASOURCE_PASSWORD).toString());
this.addTargetDataSource(configMap.get(DBContextHolder.DATASOURCE_KEY).toString(),
datasource);
}
/**
* 往数据源key-value键值对集合添加新的数据源
* @param key 新的数据源键
* @param dataSource 新的数据源
* @Author : ll. create at 2017年3月27日 下午2:56:49
*/
public void addTargetDataSource(String key, T dataSource) {
this.targetDataSources.put(key, dataSource);
super.setTargetDataSources(this.targetDataSources);
// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
super.afterPropertiesSet();
}
}
package com.eya.pubservice.datasource;
import java.sql.SQLException;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.eya.pubservice.bean.properties.DBProperties;
import com.eya.util.ProIdUtil;
/**
*
* @create ll
* @createDate 2017年3月31日 下午2:14:27
* @update
* @updateDate
*/
public class AtomikosDynamicDataSource extends AbstractDynamicDataSource<AtomikosDataSourceBean> {
/** db.properties配置信息的对应文件 */
@Autowired
private DBProperties dBProperties;
private boolean testWhileIdle = true;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;
// 是否打开连接泄露自动检测
private boolean removeAbandoned = false;
// 连接长时间没有使用,被认为发生泄露时长
private long removeAbandonedTimeoutMillis = 300 * 1000;
// 发生泄露时是否需要输出 log,建议在开启连接泄露检测时开启,方便排错
private boolean logAbandoned = false;
// 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就会被自动设定为true,使用oracle时可以设定此值。
// private int maxPoolPreparedStatementPerConnectionSize = -1;
// 配置监控统计拦截的filters
private String filters; // 监控统计:"stat" 防SQL注入:"wall" 组合使用: "stat,wall"
private List<Filter> filterList;
/*
* 创建数据源,这里创建的数据源是带连接池信息的
* @see com.eya.pubservice.datasource.IDynamicDataSource#createDataSource(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public AtomikosDataSourceBean createDataSource(String driverClassName, String url,
String username, String password) {
DruidXADataSource ds = new DruidXADataSource();
ds.setName(ProIdUtil.get16TimeRandom());
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
ds.setDriverClassName(driverClassName);
ds.setInitialSize(dBProperties.getInitialSize());
ds.setMinIdle(dBProperties.getMinIdle());
ds.setMaxActive(dBProperties.getMaxActive());
ds.setMaxWait(dBProperties.getMaxWait());
// ds.setTimeBetweenConnectErrorMillis(dBProperties.getTimeBetweenConnectErrorMillis());
ds.setTimeBetweenEvictionRunsMillis(dBProperties.getTimeBetweenEvictionRunsMillis());
ds.setMinEvictableIdleTimeMillis(dBProperties.getMinEvictableIdleTimeMillis());
// ds.setValidationQuery(dBProperties.getValidationQuery());
ds.setTestWhileIdle(testWhileIdle);
ds.setTestOnBorrow(testOnBorrow);
ds.setTestOnReturn(testOnReturn);
ds.setRemoveAbandoned(removeAbandoned);
ds.setRemoveAbandonedTimeoutMillis(removeAbandonedTimeoutMillis);
ds.setLogAbandoned(logAbandoned);
// 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就会被自动设定为true,参照druid的源码
ds.setMaxPoolPreparedStatementPerConnectionSize(
dBProperties.getMaxPoolPreparedStatementPerConnectionSize());
if (StringUtils.isNotBlank(filters))
try {
ds.setFilters(filters);
} catch (SQLException e) {
ds.close();
throw new RuntimeException(e);
}
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(ds);
// 设置数据源的唯一名称,不允许重复,按照自己的需要,生成一个不重复的值即可
atomikosDataSourceBean.setUniqueResourceName(ProIdUtil.getTimeRandomId(32));
atomikosDataSourceBean.setMaxPoolSize(dBProperties.getMaxPoolSize());
atomikosDataSourceBean.setMinPoolSize(dBProperties.getMinPoolSize());
atomikosDataSourceBean
.setBorrowConnectionTimeout(dBProperties.getBorrowConnectionTimeout());
// atomikosDataSourceBean.getXaProperties().setProperty("", arg1);
// atomikosDataSourceBean.getXaProperties().setProperty("", arg1);
// atomikosDataSourceBean.getXaProperties().setProperty("", arg1);
// atomikosDataSourceBean.getXaProperties().setProperty("", arg1);
atomikosDataSourceBean.setMaxLifetime(0);
addFilterList(ds);
return atomikosDataSourceBean;
}
private void addFilterList(DruidDataSource ds) {
if (filterList != null) {
List<Filter> targetList = ds.getProxyFilters();
for (Filter add : filterList) {
boolean found = false;
for (Filter target : targetList) {
if (add.getClass().equals(target.getClass())) {
found = true;
break;
}
}
if (!found)
targetList.add(add);
}
}
}
}
package com.eya.pubservice.datasource;
import java.util.HashMap;
import java.util.Map;
/**
* 当前正在使用的数据源信息的线程上线文
* @create ll
* @createDate 2017年3月27日 下午2:37:07
* @update
* @updateDate
*/
public class DBContextHolder {
/** 数据源的KEY */
public static final String DATASOURCE_KEY = "DATASOURCE_KEY";
/** 数据源的URL */
public static final String DATASOURCE_URL = "DATASOURCE_URL";
/** 数据源的驱动 */
public static final String DATASOURCE_DRIVER = "DATASOURCE_DRIVER";
/** 数据源的用户名 */
public static final String DATASOURCE_USERNAME = "DATASOURCE_USERNAME";
/** 数据源的密码 */
public static final String DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD";
private static final ThreadLocal<Map<String, Object>> contextHolder = new ThreadLocal<Map<String, Object>>();
public static void setDBType(Map<String, Object> dataSourceConfigMap) {
contextHolder.set(dataSourceConfigMap);
}
public static Map<String, Object> getDBType() {
Map<String, Object> dataSourceConfigMap = contextHolder.get();
if (dataSourceConfigMap == null) {
dataSourceConfigMap = new HashMap<String, Object>();
}
return dataSourceConfigMap;
}
public static void clearDBType() {
contextHolder.remove();
}
}
说明一下:
1.AbstractDynamicDataSource:该类继承AbstractRoutingDataSource,并重写determineCurrentLookupKey方法,用于确定使用哪个数据源。该类又实现了ApplicationContextAware,在Spring容器初始化的时候,会执行setApplicationContext方法,将Spring容器的上下文对象注入到属性ctx中,然后就可以使用ctx.getBean(name)来获取Spring容器中的对象。
2.AtomikosDynamicDataSource:基于atomikos的数据源类,其实我还有一个DruidDynamicDataSource类,这里没有给出,内容大部分一样,只是createDataSource的实现内容不一样,DruidDynamicDataSource只是简单地创建了一个基于Druid的数据源,而AtomikosDynamicDataSource是创建的一个可加入atomikos分布式事务的数据源。
3.DBProperties是一个基于Spring注入来获取properties内容添加的方式添加的一个和数据库配置文件(db.properties)内容相同的JAVA类(参考https://my.oschina.net/simpleton/blog/489129),用于获取 数据库配置文件(db.properties)的配置信息。我这里就不贴出该类的代码,大家可以自己想办法获取配置文件信息或直接写死都可以,毕竟这里不是本文的重点。
三、服务启动,根据配置文件,初始化数据源。MiddleTableService.java
package com.eya.pubservice.datasource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.eya.common.exception.BusinessException;
import com.eya.pubservice.InterfaceConfigService;
import com.eya.pubservice.bean.interfaceConfig.InterfaceConfigMtDetail;
import com.eya.util.ProCollection;
import com.eya.util.ProString;
/**
* 中间表服务类
* @create ll
* @createDate 2017年3月28日 上午9:36:45
* @update
* @updateDate
*/
@Component
public class MiddleTableService {
/** 日志 */
private static final Logger LOGGER = LoggerFactory.getLogger(MiddleTableService.class);
/** 接口配置服务类 */
@Autowired
private InterfaceConfigService interfaceConfigService;
/** Atomikos数据源 */
@Autowired
private AtomikosDynamicDataSource atomikosDynamicDataSource;
/**
* 随服务器启动,根据机构中间表信息配置,初始化数据源
* @Author : ll. create at 2017年3月28日 上午9:37:19
*/
public synchronized void init() {
LOGGER.info("【根据机构中间表信息配置,初始化数据源 】");
// 清空所有数据源
atomikosDynamicDataSource.getTargetDataSources().clear();
// 这段代码是获取所有的中间表数据库配置信息,读者不用关心,按照自己的方式获取配置即可
List<InterfaceConfigMtDetail> mtDetails = interfaceConfigService.getMtDetails();
if (ProCollection.isEmpty(mtDetails)) {
LOGGER.info("【无中间表配置数据,终止中间表数据源初始化任务】");
return;
}
for (InterfaceConfigMtDetail interfaceConfigMtDetail : mtDetails) {
// 判断数据源是否已经被初始化
if (atomikosDynamicDataSource.getTargetDataSources().containsKey(
interfaceConfigMtDetail.getDataSourceKey())) {
// 已经初始化
continue;
}
// 创建数据源
AtomikosDataSourceBean dataSource = atomikosDynamicDataSource.createDataSource(
interfaceConfigMtDetail.getDriver(), interfaceConfigMtDetail.getUrl(),
interfaceConfigMtDetail.getUsername(), interfaceConfigMtDetail.getPassword());
// 添加到targetDataSource中,缓存起来
atomikosDynamicDataSource.addTargetDataSource(
interfaceConfigMtDetail.getDataSourceKey(), dataSource);
}
}
/**
* 数据源控制开关,用于指定数据源
* @param interfaceCode 接口的唯一吗
* @Author : ll. create at 2017年3月28日 下午1:56:28
*/
public void dataSourceSwitch(String interfaceCode) {
InterfaceConfigMtDetail mtDetail = interfaceConfigService.getMtDetailMap().get(
interfaceCode);
if (mtDetail == null) {
throw new BusinessException("根据接口唯一码未获取到中间表配置信息");
}
if (ProString.isBlank(mtDetail.getDriver()) && ProString.isBlank(mtDetail.getUrl())
&& ProString.isBlank(mtDetail.getUsername())) {
throw new IllegalArgumentException(String.format("接口【%s】未配置中间表信息,无法切换数据源",
interfaceCode));
}
Map<String, Object> dataSourceConfigMap = new HashMap<String, Object>();
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_KEY, mtDetail.getDataSourceKey());
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_DRIVER, mtDetail.getDriver());
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_URL, mtDetail.getUrl());
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_USERNAME, mtDetail.getUsername());
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_PASSWORD, mtDetail.getPassword());
LOGGER.info("【指定数据源】dataSourceConfigMap = {}", dataSourceConfigMap);
DBContextHolder.setDBType(dataSourceConfigMap);
}
}
说明:
1.该类的init方法在服务器启动的时候执行,根据中间表配置信息,初始化数据源。
2.dataSourceSwitch:数据源开关,根据接口唯一码(一个唯一码对应一类业务,同一业务和不同系统的交互方式可能不同,如使用中间库、webService等)判断是否使用中间库,如果是,则根据信息,指定到对应的数据源。
3.这里引发了一个思考:一个数据源中,为何又要有多个不同的数据源?比如文中的AtomikosDynamicDataSource,拥有一个targetDataSources属性(该属性在父类AbstractRoutingDataSource中),存储多个不同的数据源。虽然这样的设计解决了博主的问题,但是目前搞不明白这样设计的目的。
4.通常意义的数据源是指javax.sql.DataSource类,但是它的子类不仅仅只有数据源信息,还包含了连接池属性,如DruidXADataSource。初始化数据源的时候,指定了连接池的信息,所以后面切换了数据源之后,没有每次都创建连接等操作,直接从连接池取现有的空闲连接。
2019-3-26 补充:近日帮很多朋友答疑的时候发现,博文中缺少一个更为简单的示例,缺少对工作原理的说明,今天补充一下
先看下面一段示例代码,有2个方法,saveUser2DB1和saveUser2DB2,都是保存用户信息,但是存入的分别是testUserDB1和testUserDB2。
工作原理:切换数据源的关键,就是利用DBContextHolder上下文对象,把数据源的信息放进去,然后在执行SQL之前,会调用AtomikosDynamicDataSource的determineCurrentLookupKey方法,该方法返回的其实就是targetDataSources的key值。在determineCurrentLookupKey方法中,调用了verifyAndInitDataSource方法,判断对应的数据源是否已经初始化,没有,则先初始化,再把初始化得到的DataSource子类(本文中就是AtomikosDynamicDataSource)对象,放入到targetDataSources中,并返回对应的key,数据源切换就完成了,后面执行SQL的时候,就是往对应的数据源中添加了。
@Autowired
private UserDao userDao;
public void saveUser2DB1(User user){
// 切换数据源。使用上下文对象标记当前需要使用的数据源
Map<String, Object> dataSourceConfigMap = new HashMap<String, Object>();
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_KEY, "testUserDB1");
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_DRIVER, "com.mysql.jdbc.Driver");
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_URL, "jdbc:mysql://localhost:3306/testUserDB1");
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_USERNAME, "root");
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_PASSWORD, "root");
LOGGER.info("【指定数据源】dataSourceConfigMap = {}", dataSourceConfigMap);
DBContextHolder.setDBType(dataSourceConfigMap);
userDao.save(user);
}
public void saveUser2DB2(User user){
// 切换数据源。使用上下文对象标记当前需要使用的数据源
Map<String, Object> dataSourceConfigMap = new HashMap<String, Object>();
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_KEY, "testUserDB2");
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_DRIVER, "com.mysql.jdbc.Driver");
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_URL, "jdbc:mysql://localhost:3306/testUserDB2");
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_USERNAME, "root");
dataSourceConfigMap.put(DBContextHolder.DATASOURCE_PASSWORD, "root");
LOGGER.info("【指定数据源】dataSourceConfigMap = {}", dataSourceConfigMap);
DBContextHolder.setDBType(dataSourceConfigMap);
userDao.save(user);
}
以上内容,完全由博主自己集合网上资料摸索出来的一套业务解决方案,如有缺陷,敬请大家指出交流。
如果有问题,大家可以留言讨论,或加我QQ/发邮件交流,联系方式在我个人资料中。由于平时工作较忙,如未及时回复大家,敬请见谅。