天天看點

基于spring的aop實作多資料源動态切換

 一、動态切換資料源理論知識

 項目中我們經常會遇到多資料源的問題,尤其是資料同步或定時任務等項目更是如此;又例如:讀寫分離資料庫配置的系統。

1、相信很多人都知道JDK代理,分靜态代理和動态代理兩種,同樣的,多資料源設定也分為類似的兩種:

1)靜态資料源切換:

一般情況下,我們可以配置多個資料源,然後為每個資料源寫一套對應的sessionFactory和dao層,我們稱之為靜态資料源配置,這樣的好處是想調用那個資料源,直接調用dao層即可。但缺點也很明顯,每個Dao層代碼中寫死了一個SessionFactory,這樣日後如果再多一個資料源,還要改代碼添加一個SessionFactory,顯然這并不符合開閉原則。

2)動态資料源切換:

配置多個資料源,隻對應一套sessionFactory,根據需要,資料源之間可以動态切換。       

    2、動态資料源切換時,如何保證資料庫的事務:

    目前事務最靈活的方式,是使用spring的聲明式事務,本質是利用了spring的aop,在執行資料庫操作前後,加上事務處理。

    spring的事務管理,是基于資料源的,是以如果要實作動态資料源切換,而且在同一個資料源中保證事務是起作用的話,就需要注意二者的順序問題,即:在事物起作用之前就要把資料源切換回來。

    舉一個例子:web開發常見是三層結構:controller、service、dao。一般事務都會在service層添加,如果使用spring的聲明式事物管理,在調用service層代碼之前,spring會通過aop的方式動态添加事務控制代碼,是以如果要想保證事物是有效的,那麼就必須在spring添加事務之前把資料源動态切換過來,也就是動态切換資料源的aop要至少在service上添加,而且要在spring聲明式事物aop之前添加.根據上面分析:

    最簡單的方式是把動态切換資料源的aop加到controller層,這樣在controller層裡面就可以确定下來資料源了。不過,這樣有一個缺點就是,每一個controller綁定了一個資料源,不靈活。對于這種:一個請求,需要使用兩個以上資料源中的資料完成的業務時,就無法實作了。

    針對上面的這種問題,可以考慮把動态切換資料源的aop放到service層,但要注意一定要在事務aop之前來完成。這樣,對于一個需要多個資料源資料的請求,我們隻需要在controller裡面注入多個service實作即可。但這種做法的問題在于,controller層裡面會涉及到一些不必要的業務代碼,例如:合并兩個資料源中的list…

此外,針對上面的問題,還可以再考慮一種方案,就是把事務控制到dao層,然後在service層裡面動态切換資料源。

二、下面是我在實際項目中的一點應用:

    1、首先,要有資料庫的相關配置檔案jdbc.properties:

#mysql
cms.mysql.driver=com.mysql.jdbc.Driver
cms.mysql.url=jdbc:mysql://127.0.0.1:3306/VSRecStream?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
cms.mysql.username=root
cms.mysql.password=******

edition.mysql.driver=com.mysql.jdbc.Driver
edition.mysql.url=jdbc:mysql://127.0.0.1:3306/ResourcePublish?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
edition.mysql.username=root
edition.mysql.password=******      

    2、有了資料源,肯定需要将這些源管理起來,此時很多人肯定想到了spring,對的,看下面:

<!-- cms configuration -->
	<bean id="cmsBaseDataSource" class="org.apache.commons.dbcp.BasicDataSource"
		destroy-method="close">
		<property name="driverClassName" value="${cms.mysql.driver}" />
		<property name="url" value="${cms.mysql.url}" />
		<property name="username" value="${cms.mysql.username}" />
		<property name="password" value="${cms.mysql.password}" />
        <property name="maxActive" value="20" />
        <property name="initialSize" value="5" />
        <property name="maxWait" value="60000" />
        <property name="minIdle" value="5" />
        <property name="maxIdle" value="20" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="180"/>  
        <property name="validationQuery" value="select 1" />
    </bean>
    
    <!-- publish configuration -->
    <bean id="editionBaseDataSource" class="org.apache.commons.dbcp.BasicDataSource"
		destroy-method="close">
		<property name="driverClassName" value="${edition.mysql.driver}" />
		<property name="url" value="${edition.mysql.url}" />
		<property name="username" value="${edition.mysql.username}" />
		<property name="password" value="${edition.mysql.password}" />
        <property name="maxActive" value="20" />
        <property name="initialSize" value="5" />
        <property name="maxWait" value="60000" />
        <property name="minIdle" value="5" />
        <property name="maxIdle" value="20" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="180"/>  
        <property name="validationQuery" value="select 1" />
    </bean>      

    3、上面的資料源倒是配置起來了,但是怎麼樣才能實作一個sessionFactory來管理兩個源呢,肯定是需要一個動态的代理類,寫一個DynamicDataSource類繼承 AbstractRoutingDataSource ,并實作 determineCurrentLookupKey方法即可,AbstractRoutingDataSource是spring裡的一個實作類,有興趣的朋友可以研究一下他的源碼,在此,不做過多介紹。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 動态分派資料源
 * @ClassName: DynamicDataSource   
 * @author dove
 * @date 2017年3月21日 
 *
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		return CustomerContextHolder.getCustomerType();
	}

}
           

    利用ThreadLocal解決線程安全問題

package com.visionvera.common;


public class CustomerContextHolder {
    //用ThreadLocal來設定目前線程使用哪個dataSource
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    
    public static void setCustomerType(String customerType) {
        contextHolder.set(customerType);
    }
    public static String getCustomerType() {
        return contextHolder.get();
    }
    
    public static void clearCustomerType() {
        contextHolder.remove();
    }
}
           

    4、動态類編寫完畢,就要用起來,實作一個sessionFactory管理多個資料源

<!--統一的dataSource-->
	<bean id="dynamicDataSource" class="com.visionvera.common.DynamicDataSource" >
	    <property name="targetDataSources">
	        <map key-type="java.lang.String">
	            <!--通過不同的key決定用哪個dataSource-->
	            <entry key="cmsBaseDataSource" value-ref="cmsBaseDataSource"></entry>
	            <entry key="editionBaseDataSource" value-ref="editionBaseDataSource" ></entry>
	        </map>
	    </property>
	    <!--設定預設的dataSource-->
	    <property name="defaultTargetDataSource" ref="cmsBaseDataSource"></property>
	</bean>      
<!-- define the SqlSessionFactory -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dynamicDataSource" />
		<property name="typeAliasesPackage" value="com.visionvera.cms.bean,com.visionvera.cms.vo,com.visionvera.edition.bean,com.visionvera.edition.vo"/>
        <!-- 自動掃描mapping.xml檔案 -->
        <property name="mapperLocations" value="classpath:com/visionvera/*/dao/mapper/*.xml"/>
		
	</bean>      

    5、以上代碼完成後,基本可以在每次調用service之前,通過手動切換資料源,即執行CustomerContextHolder.setCustomerType("cmsDataSource"),實作資料源的切換了,但是這樣的話,完全達不到我們想要的動态切換資料源的需求。

    那麼我通過網上查找,發現有的朋友是通過寫自定義注解配合攔截器來實作動态資料源切換的,但我個人感覺,如果在前期一直使用一個資料源的項目,後期突然要加入新的資料源的情況來說,不太适合,因為,這樣的話,需要在每一個dao中添加注解,這樣,之前的項目代碼也需要修改,這也不是很好。而我的實作方法是,利用sping aop定義切面,在切面中實作資料源的切換,請看下面的代碼:

    編寫切面代碼:

package com.visionvera.common.aspect;

import java.util.Map;
import java.util.Map.Entry;

import org.aspectj.lang.JoinPoint;

import com.visionvera.common.CustomerContextHolder;
/**
 * 動态切換資料源切面
 * @ClassName: DataSourceAspect   
 * @author chenting
 * @date 2017年3月22日 
 *
 */
public class DataSourceAspect{
	
	private String defaultDataSource;
	private Map<String, Object> targetDataSources;
	
	public void doBefore(JoinPoint joinPoint) {
	 	boolean isSetDataSource = false;
	 	String targetName = joinPoint.getTarget().getClass().getName();
	 	for(Entry<String, Object> entry : targetDataSources.entrySet()) {
	 		if(targetName.contains(entry.getKey())){
	 			String value = entry.getValue().toString();
	 			CustomerContextHolder.setCustomerType(value);
	 			isSetDataSource = true;
	 			break;
	 		}
	 	}
	 	if(!isSetDataSource) {
	 		CustomerContextHolder.setCustomerType(defaultDataSource);
	 	}
	 	
	}
	
	public void doAfterReturning(JoinPoint joinPoint) {
		CustomerContextHolder.clearCustomerType();
	}

	public Map<String, Object> getTargetDataSources() {
		return targetDataSources;
	}

	public void setTargetDataSources(Map<String, Object> targetDataSources) {
		this.targetDataSources = targetDataSources;
	}

	public String getDefaultDataSource() {
		return defaultDataSource;
	}

	public void setDefaultDataSource(String defaultDataSource) {
		this.defaultDataSource = defaultDataSource;
	}
	
}
           

     在配置檔案中利用spring aop進行管理

<!-- 配置資料源切換切面 -->
 	<bean id="dataSourceChangeAspect" class="com.visionvera.common.aspect.DataSourceAspect">
 		<property name="defaultDataSource" value="cmsBaseDataSource"></property>
 		<property name="targetDataSources">
	        <map key-type="java.lang.String">
	            <!--通過不同的key決定用哪個dataSource-->
	            <entry key="com.visionvera.cms" value="cmsBaseDataSource"></entry>
	            <entry key="com.visionvera.edition" value="editionBaseDataSource" ></entry>
	        </map>
	    </property>
 	</bean>      

    上面map中配置的方式,主要是我仿照上面DynamicDataSource的模式來寫的,DynamicDataSource是繼續子父類的,而我這個是自己寫的,當然想實作的功能是類似的。這樣配置的目的是想實作,同一個包下的dao接口,使用同一個資料源,這樣肯定也有局限性,在後期如果遇到的話,會進行優化。

<aop:config>
		<aop:pointcut id="serviceAop"
			expression="execution(* com.visionvera.*.service..*.*(..)))" />
		<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceAop" order="2"/>
		
		<!-- 配置資料源的動态切換 -->
		<aop:aspect ref="dataSourceChangeAspect" order="1">
			<aop:before method="doBefore" pointcut-ref="serviceAop"/>
			<aop:after-returning method="doAfterReturning" pointcut-ref="serviceAop"/>
		</aop:aspect>
		
	</aop:config>      

    上面之是以跟事物控制放一塊,是因為切換資料源跟事務的切入點相同,故此寫在一起,當然完全可以分開寫。

     分開寫的模式為:

<aop:config>  
        <aop:aspect id="dataSourceAspect" ref="dataSourceChangeAspect">  
            <aop:pointcut id="daoAop" expression="execution(* com.visionvera.*.service..*.*(..)))" />  
            <aop:before method="doBefore" pointcut-ref="daoAop"/>  
            <aop:after-returning method="doAfterReturning" pointcut-ref="daoAop"/>
        </aop:aspect>  
    </aop:config>        

 至此,所有的配置均已完成。特别需要注意的一點是,order這一項,配置了執行的優先級,一定要在事務開啟前,将資料源切換完畢。

但是還有一個比較大的問題沒有解決,假如有兩個資料源,此時需要同時添加一條資料,此時如果出現插入異常的話,事務是沒法保證兩個資料源都能復原的,這個等待大神給指點迷津,謝謝。。