Spring動態切換多數據源解決方案


 

Spring動態配置多數據源,即在大型應用中對數據進行切分,並且采用多個數據庫實例進行管理,這樣可以有效提高系統的水平伸縮性。而這樣的方案就會不同於常見的單一數據實例的方案,這就要程序在運行時根據當時的請求及系統狀態來動態的決定將數據存儲在哪個數據庫實例中,以及從哪個數據庫提取數據。

 

Spring配置多數據源的方式和具體使用過程。 
Spring對於多數據源,以數據庫表為參照,大體上可以分成兩大類情況: 
一是,表級上的跨數據庫。即,對於不同的數據庫卻有相同的表(表名和表結構完全相同)。 
二是,非表級上的跨數據庫。即,多個數據源不存在相同的表。

 

1、根據用戶的選擇,使用不同的數據源。

 

2、解決思路鎖定:將sessionFactory的屬性dataSource設置成不同的數據源,以達到切換數據源的目的。

 

3、問題產生:因為整個項目用的幾乎都是單例模式,當多個用戶並發訪問數據庫的時候,會產生資源爭奪的問題。即項目啟動時候,所有的bean都被裝載到內存,並且每個bean都只有一個對象。正因為只有一個對象,所有的對象屬性就如同靜態變量(靜態變量跟單例很相似,常用靜態來實現單例)。整個項目系統的dataSource只有一個,如果很多用戶不斷的去改變dataSource的值,那必然會出現資源的掠奪問題,造成系統隱患。

 

4、多資源共享解決思路:同一資源被搶奪的時候,通常有兩種做法,a、以時間換空間 b、以空間換時間。

 

5、線程同步機制就是典型的“以時間換空間”,采用排隊稍等的方法,一個個等待,直到前面一個用完,后面的才跟上,多人共用一個變量,用synchronized鎖定排隊。   

 

6、“ThreadLocal”就是典型的“以空間換時間”,她可以為每一個人提供一份變量,因此可以同時訪問並互不干擾。

 

7、言歸正傳:sessionFactory的屬性dataSource設置成不用的數據源,首先不能在配置文件中寫死,我們必須為她單獨寫一個類,讓她來引用這個類,在這個類中再來判斷我們到底要選擇哪個數據源。

 

 

 

spring + mybatis 多數據源切換

 

DbContextHolder.java

 

 1 package com.easyway.stage.commons;  
 2   
 3 public class DbContextHolder  
 4 {  
 5   
 6     // ThreadLocal是線程安全的,並且不能在多線程之間共享。  
 7     private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();  
 8   
 9     public static void setDbType(String dbType)  
10     {  
11         contextHolder.set(dbType);  
12     }  
13   
14     public static String getDbType()  
15     {  
16         return ((String) contextHolder.get());  
17     }  
18   
19     public static void clearDbType()  
20     {  
21         contextHolder.remove();  
22     }  
23   
24 }  

 

 

MultiDataSource.java

 

 1 package com.easyway.stage.commons;  
 2   
 3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;  
 4   
 5 public class MultiDataSource extends AbstractRoutingDataSource  
 6 {  
 7   
 8     @Override  
 9     protected Object determineCurrentLookupKey()  
10     {  
11         return  DbContextHolder.getDbType();  
12     }  
13   
14 }  
 

 

 

Xml代碼:

applicationContext.xml

 
 1 <?xml version="1.0" encoding="UTF-8"?>  
 2 <beans  
 3     xmlns="http://www.springframework.org/schema/beans"  
 4     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"  
 5     xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"  
 6     xmlns:context="http://www.springframework.org/schema/context"  
 7     xsi:schemaLocation="         
 8       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
 9       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd  
10       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd  
11       http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd  
12       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">  
13           
14     <context:annotation-config/>  
15   
16     <!-- 數據源 -->    
17     <bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource">  
18         <!-- 配置初始化大小、最小、最大 -->  
19         <property name="initialSize" value="1" />  
20         <property name="maxActive" value="20" />   
21         <property name="minIdle" value="1" />  
22           
23         <!-- 配置獲取連接等待超時的時間60s -->  
24         <property name="maxWait" value="60000" />   
25           
26         <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 -->  
27         <property name="timeBetweenEvictionRunsMillis" value="60000" />  
28         <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->    
29         <property name="minEvictableIdleTimeMillis" value="300000" />    
30          
31         <property name="validationQuery" value="SELECT 'x'" />    
32         <property name="testWhileIdle" value="true" />    
33         <property name="testOnBorrow" value="false" />    
34         <property name="testOnReturn" value="false" />    
35           
36         <!-- 打開PSCache,並且指定每個連接上PSCache的大小 -->  
37         <property name="poolPreparedStatements" value="true" />  
38         <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />  
39           
40         <!-- 配置監控統計攔截的filters -->  
41         <property name="filters" value="wall,stat,slf4j" />  
42           
43         <!-- 對於長時間不使用的連接強制關閉  -->  
44         <property name="removeAbandoned" value="true" />  
45         <!-- 超過30分鍾開始關閉空閑連接 -->  
46         <property name="removeAbandonedTimeout" value="1800" />   
47         <!-- 關閉abanded連接時輸出錯誤日志 -->   
48         <property name="logAbandoned" value="true" />    
49     </bean>    
50     <bean id="local" parent="parentDataSource" init-method="init" destroy-method="close">      
51         <property name="url" value="${local.jdbc.url}" />    
52         <property name="username" value="${local.jdbc.username}" />    
53         <property name="password" value="${local.jdbc.password}" />      
54     </bean>  
55     <bean id="server" parent="parentDataSource" init-method="init" destroy-method="close">      
56         <property name="url" value="${jdbc.url}" />    
57         <property name="username" value="${jdbc.username}" />    
58         <property name="password" value="${jdbc.password}" />      
59     </bean>  
60   
61     <bean id="dataSource" class="com.autrade.stage.commons.MultiDataSource">  
62         <property name="targetDataSources">  
63             <map key-type="java.lang.String">  
64                 <entry value-ref="local" key="local"></entry>  
65                 <entry value-ref="server" key="server"></entry>  
66             </map>  
67         </property>  
68         <!-- 默認使用server的數據源 -->  
69         <property name="defaultTargetDataSource" ref="server"></property>  
70     </bean>    
71   
72     <!-- MyBatis -->  
73     <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
74         <property name="dataSource" ref="dataSource" />  
75         <property name="configLocation" value="classpath:resources/mybatis/myBatisConfig.xml" />  
76         <property name="mapperLocations" value="classpath:resources/mybatis/mapper/*.xml"/>  
77     </bean>  
78     <bean class="org.mybatis.spring.SqlSessionTemplate">  
79       <constructor-arg ref="sqlSessionFactory"/>  
80     </bean>  
81     <!-- MyBatis -->  
82       
83     <!-- 配置事務管理對象-->  
84     <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
85         <property name="dataSource" ref="dataSource"/>  
86     </bean>  
87     <!-- 將所有具有@Transactional注解的Bean自動配置為聲明式事務支持 -->  
88     <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>  
89   
90     <!-- 自定義的攔截器 -->  
91     <bean id="methodAdvisor" class="com.easyway.app.interceptor.InjectorManager" />  
92   
93     <aop:config proxy-target-class="true">  
94         <aop:pointcut id="baseMethods"  
95             expression="execution(* com.easyway.app.service..*.*(..))" />  
96         <aop:advisor advice-ref="methodAdvisor" pointcut-ref="baseMethods" />  
97     </aop:config>  
98         
99 </beans>  

 

Test.java測試類

 

 1 package com.easyway.stage.test;  
 2   
 3 import javax.sql.DataSource;  
 4   
 5 import org.apache.ibatis.session.SqlSession;  
 6 import org.apache.ibatis.session.SqlSessionFactory;  
 7 import org.mybatis.spring.SqlSessionFactoryBean;  
 8 import org.springframework.context.ApplicationContext;  
 9 import org.springframework.context.support.ClassPathXmlApplicationContext;  
10 import org.springframework.core.io.FileSystemResource;  
11 import org.springframework.core.io.Resource;  
12   
13 import com.easyway.stage.commons.DbContextHolder;  
14   
15 public class Test  
16 {  
17   
18     /** 
19      * @param args 
20      */  
21     public static void main(String[] args)  
22     {  
23         ApplicationContext appContext = new ClassPathXmlApplicationContext("client-beans.xml");  
24   
25         DbContextHolder.setDbType("local");  
26         String res = "resources/mybatis/myBatisConfig.xml";  
27         DataSource datasource = (DataSource) appContext.getBean("dataSource");  
28   
29         SqlSessionFactoryBean bean = new SqlSessionFactoryBean();  
30         bean.setDataSource(datasource);  
31         Resource resource = new FileSystemResource(res);  
32         bean.setConfigLocation(resource);  
33         try  
34         {  
35             SqlSessionFactory sessionfactory = bean.getObject();  
36             SqlSession session = sessionfactory.openSession();  
37             User user = session.selectOne("com.easyway.mybatis.mapper.findOne");  
38             System.out.println(user.getName());  
39         }  
40         catch (Exception e)  
41         {  
42             e.printStackTrace();  
43         }  
44   
45         DbContextHolder.setDbType("server");  
46         String res1 = "resources/mybatis/myBatisConfig.xml";  
47         DataSource datasource1 = (DataSource) appContext.getBean("dataSource");  
48   
49         SqlSessionFactoryBean bean1 = new SqlSessionFactoryBean();  
50         bean1.setDataSource(datasource1);  
51         Resource resource1 = new FileSystemResource(res1);  
52         bean1.setConfigLocation(resource1);  
53   
54         try  
55         {  
56             SqlSessionFactory sessionfactory = bean.getObject();  
57             SqlSession session = sessionfactory.openSession();  
58             User user = session.selectOne("com.easyway.mybatis.mapper.findOne");  
59             System.out.println(user.getName());  
60         }  
61         catch (Exception e)  
62         {  
63             e.printStackTrace();  
64         }  
65   
66     }  
67   
68 }  

 

注意:當切換數據源時,需要在service層之外,如果需要在service層中切換非默認數據源,則不能開啟事務,而且下次使用時,線程仍然綁定,此時若需要使用默認數據源,則需要顯示的手動切換數據源,否則會出現xxx.table doesnt exist的問題。

 

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

 

Spring動態配置多數據源,即在大型應用中對數據進行切分,並且采用多個數據庫實例進行管理,這樣可以有效提高系統的水平伸縮性。而這樣的方案就會不同於常見的單一數據實例的方案,這就要程序在運行時根據當時的請求及系統狀態來動態的決定將數據存儲在哪個數據庫實例中,以及從哪個數據庫提取數據。

       Spring2.x以后的版本中采用Proxy模式,就是我們在方案中實現一個虛擬的數據源,並且用它來封裝數據源選擇邏輯,這樣就可以有效地將數據源選擇邏輯從Client中分離出來。Client提供選擇所需的上下文(因為這是Client所知道的),由虛擬的DataSource根據Client提供的上下文來實現數據源的選擇。

實現

具體的實現就是,虛擬的DataSource僅需繼承AbstractRoutingDataSource實現determineCurrentLookupKey()在其中封裝數據源的選擇邏輯。
 
一、動態配置多數據源
1. 數據源的名稱常量類:
[java] view plain copy
 
 print?
  1. /** 
  2.  * 動態配置多數據源 
  3.  * 數據源的名稱常量類 
  4.  * @author LONGHUI_LUO 
  5.  * 
  6.  */  
  7. public class DataSourceConst {  
  8.     public static final String TEST="test";  
  9.     public static final String USER="User";  
  10. }  

2. 建立一個獲得和設置上下文環境的類,主要負責改變上下文數據源的名稱:
 
[java] view plain copy
 
 print?
  1. /** 
  2.  * 獲得和設置上下文環境 主要負責改變上下文數據源的名稱 
  3.  *  
  4.  * @author LONGHUI_LUO 
  5.  *  
  6.  */  
  7. public class DataSourceContextHolder {  
  8.     private static final ThreadLocal contextHolder = new ThreadLocal(); // 線程本地環境  
  9.   
  10.     // 設置數據源類型  
  11.     public static void setDataSourceType(String dataSourceType) {  
  12.         contextHolder.set(dataSourceType);  
  13.     }  
  14.   
  15.     // 獲取數據源類型  
  16.     public static String getDataSourceType() {  
  17.         return (String) contextHolder.get();  
  18.     }  
  19.   
  20.     // 清除數據源類型  
  21.     public static void clearDataSourceType() {  
  22.         contextHolder.remove();  
  23.     }  
  24.   
  25. }  


3. 建立動態數據源類,注意,這個類必須繼承AbstractRoutingDataSource,且實現方法determineCurrentLookupKey,該方法返回一個Object,一般是返回字符串:

 
[java] view plain copy
 
 print?
  1. /** 
  2.  * 建立動態數據源 
  3.  *  
  4.  * @author LONGHUI_LUO 
  5.  *  
  6.  */  
  7. public class DynamicDataSource extends AbstractRoutingDataSource {  
  8.   
  9.  protected Object determineCurrentLookupKey() {  
  10.   // 在進行DAO操作前,通過上下文環境變量,獲得數據源的類型  
  11.   return DataSourceContextHolder.getDataSourceType();  
  12.  }  
  13.   
  14. }  


4. 編寫spring的配置文件配置多個數據源

[html] view plain copy
 
 print?
  1.         <!-- 數據源相同的內容 -->  
  2. <bean  
  3.         id="parentDataSource"  
  4.         class="org.apache.commons.dbcp.BasicDataSource"  
  5.         destroy-method="close">  
  6.         <property  
  7.             name="driverClassName"  
  8.             value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />  
  9.         <property name="username" value="sa" />  
  10.         <property name="password" value="net2com" />  
  11. </bean>  
[html] view plain copy
 
 print?
  1. <!-- start以下配置各個數據源的特性 -->  
  2. <bean parent="parentDataSource" id="testDataSource">   
  3.         <propertynamepropertyname="url" value="jdbc:sqlserver://localhost:1433;databaseName=test" />  
  4. </bean>   
  5. <bean parent="parentDataSource" id="UserDataSource">   
  6.             <property  
  7.             name="url"  
  8.             value="jdbc:sqlserver://localhost:1433;databaseName=User" />  
  9. </bean>   
[html] view plain copy
 
 print?
  1. <!-- end 配置各個數據源的特性 -->  


5. 編寫spring配置文件配置多數據源映射關系

[html] view plain copy
 
 print?
  1. <bean class="com.xxxx.datasouce.DynamicDataSource" id="dataSource">  
  2.     <property name="targetDataSources">   
  3.        <map key-type="java.lang.String">   
  4.            <entry value-ref="testDataSource" key="test"></entry>  
  5.            <entry value-ref="UserDataSource" key="User"></entry>  
  6.        </map>   
  7.     </property>   
  8.     <property name="defaultTargetDataSource" ref="testDataSource" ></property>  
  9. </bean>  

        在這個配置中第一個property屬性配置目標數據源,<map key-type="Java.lang.String">中的key-type必須要和靜態鍵值對照類DataSourceConst中的值的類型相 同;<entry key="User" value-ref="userDataSource"/>中key的值必須要和靜態鍵值對照類中的值相同,如果有多個值,可以配置多個< entry>標簽。第二個property屬性配置默認的數據源。

 

動態切換是數據源

[java] view plain copy
 
 print?
  1. DataSourceContextHolder.setDataSourceType(DataSourceConst.TEST);  


 

該方案的優勢

       首先,這個方案完全是在spring的框架下解決的,數據源依然配置在spring的配置文件中,sessionFactory依然去配置它的dataSource屬性,它甚至都不知道dataSource的改變。唯一不同的是在真正的dataSource與sessionFactory之間增加了一個MultiDataSource。
其次,實現簡單,易於維護。這個方案雖然我說了這么多東西,其實都是分析,真正需要我們寫的代碼就只有MultiDataSource、SpObserver兩個類。MultiDataSource類真正要寫的只有getDataSource()和getDataSource(sp)兩個方法,而SpObserver類更簡單了。實現越簡單,出錯的可能就越小,維護性就越高。
最后,這個方案可以使單數據源與多數據源兼容。這個方案完全不影響BUS和DAO的編寫。如果我們的項目在開始之初是單數據源的情況下開發,隨着項目的進行,需要變更為多數據源,則只需要修改spring配置,並少量修改MVC層以便在請求中寫入需要的數據源名,變更就完成了。如果我們的項目希望改回單數據源,則只需要簡單修改配置文件。這樣,為我們的項目將增加更多的彈性。

該方案的缺點

       沒有能夠解決多用戶訪問單例“sessionFactory”時共享“dataSource”變量,導致產生爭搶“dataSource”的結果,本質類似於操作系統中的“生產者消費者”問題。因此當多用戶訪問時,多數據源可能會導致系統性能下降的后果。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM