JAVA中使用代码创建多数据源,并实现动态切换(二)-集成分布式事务

    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/发邮件交流,联系方式在我个人资料中。由于平时工作较忙,如未及时回复大家,敬请见谅

转载于:https://my.oschina.net/simpleton/blog/916108

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值