JAVA中使用代碼建立多數據源,並實現動態切換(二)-集成分佈式事務

    2017年8月16日11:45:39更新:這裏貼出我本身寫的一個demo,放在CSDN上了。http://download.csdn.net/download/llzhaoyun/9912658java

    前面寫了一篇關於使用Spring + druid + Mybatis配置多數據源,並在代碼中動態選擇使用不一樣的數據源的博文,當時寫該文時,因爲比較忙,只簡單作了記錄,後來又作了一些擴展,添加了分佈式事務等,今天就另起一文,詳細記錄下目前生產線上正在使用的穩定版本。mysql

    業務場景:web

    有一個系統A做爲數據中轉服務器。若干個系統數據都須要通過系統A作處理、轉發,而數據交互有多種方式,如WebService、REST API、中間表等,A也會操做本身的數據庫。本文主要圍繞操做中間表的方式來說。spring

    A須要操做的本身的數據庫是肯定的,但因爲接入的系統可能隨業務發展而增長或改變,因此不能將中間表的鏈接信息寫死,須要使用配置文件維護,並在服務器啓動的時候動態加載,而後由不一樣的業務去決定使用哪個數據源。(該段文本描述的業務問題已在上篇文章https://my.oschina.net/simpleton/blog/868608中解決)sql

    因爲系統A會讀中間表,而後將讀取的數據預處理,而後存入A系統的數據庫或其餘中間庫,若是使用DataSourceTransactionManager做爲事務管理器來控制事務,會出現多數據庫事務不一致的問題,那麼,解決這類問題有兩種方案:編程式事務和分佈式事務。若是使用編程式事務的話,須要在每一個地方顯示地管理實務(開啓、提交、回滾),耦合較高。對於複雜的業務來講,很差處理,故不適合系統A的業務場景。後來看了分佈式事務框架atomikos,以爲應該能解決我目前的問題,經測試,效果很贊,下面就讓咱們一塊兒來看看它是什麼集成到系統的。(原理的話,你們本身百度一下)數據庫

    好了,說了這麼多,下面來一步一步解決咱們問題。apache

    1、配置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便可)就能夠了,這也是網上目前大部分資料已經完成的內容。

    2、建立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)的配置信息。我這裏就不貼出該類的代碼,你們能夠本身想辦法獲取配置文件信息或直接寫死均可以,畢竟這裏不是本文的重點。

    3、服務啓動,根據配置文件,初始化數據源。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/發郵件交流,聯繫方式在我我的資料中。因爲平時工做較忙,如未及時回覆你們,敬請見諒

相關文章
相關標籤/搜索