多資料來源切換
轉載:https://www.cnblogs.com/wyb628/p/7240061.html(原理分析,未考慮事務)
https://blog.csdn.net/qq_37061442/article/details/82350258(事務,及其解決方案)
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
- /**
- * 動態配置多資料來源
- * 資料來源的名稱常量類
- * @author LONGHUI_LUO
- *
- */
- public class DataSourceConst {
- public static final String TEST="test";
- public static final String USER="User";
- }
2. 建立一個獲得和設定上下文環境的類,主要負責改變上下文資料來源的名稱:
[java] view plain copy
- /**
- * 獲得和設定上下文環境 主要負責改變上下文資料來源的名稱
- *
- * @author LONGHUI_LUO
- *
- */
- public class DataSourceContextHolder {
- private static final ThreadLocal contextHolder = new ThreadLocal(); // 執行緒本地環境
- // 設定資料來源型別
- public static void setDataSourceType(String dataSourceType) {
- contextHolder.set(dataSourceType);
- }
- // 獲取資料來源型別
- public static String getDataSourceType() {
- return (String) contextHolder.get();
- }
- // 清除資料來源型別
- public static void clearDataSourceType() {
- contextHolder.remove();
- }
- }
3. 建立動態資料源類,注意,這個類必須繼承AbstractRoutingDataSource,且實現方法determineCurrentLookupKey,該方法返回一個Object,一般是返回字串:
[java] view plain copy
- /**
- * 建立動態資料來源
- *
- * @author LONGHUI_LUO
- *
- */
- public class DynamicDataSource extends AbstractRoutingDataSource {
- protected Object determineCurrentLookupKey() {
- // 在進行DAO操作前,通過上下文環境變數,獲得資料來源的型別
- return DataSourceContextHolder.getDataSourceType();
- }
- }
4. 編寫spring的配置檔案配置多個數據源
[html] view plain copy
- <!-- 資料來源相同的內容 -->
- <bean
- id="parentDataSource"
- class="org.apache.commons.dbcp.BasicDataSource"
- destroy-method="close">
- <property
- name="driverClassName"
- value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
- <property name="username" value="sa" />
- <property name="password" value="net2com" />
- </bean>
[html] view plain copy
- <!-- start以下配置各個資料來源的特性 -->
- <bean parent="parentDataSource" id="testDataSource">
- <propertynamepropertyname="url" value="jdbc:sqlserver://localhost:1433;databaseName=test" />
- </bean>
- <bean parent="parentDataSource" id="UserDataSource">
- <property
- name="url"
- value="jdbc:sqlserver://localhost:1433;databaseName=User" />
- </bean>
[html] view plain copy
- <!-- end 配置各個資料來源的特性 -->
5. 編寫spring配置檔案配置多資料來源對映關係
[html] view plain copy
- <bean class="com.xxxx.datasouce.DynamicDataSource" id="dataSource">
- <property name="targetDataSources">
- <map key-type="java.lang.String">
- <entry value-ref="testDataSource" key="test"></entry>
- <entry value-ref="UserDataSource" key="User"></entry>
- </map>
- </property>
- <property name="defaultTargetDataSource" ref="testDataSource" ></property>
- </bean>
在這個配置中第一個property屬性配置目標資料來源,<map key-type="Java.lang.String">中的key-type必須要和靜態鍵值對照類DataSourceConst中的值的型別相 同;<entry key="User" value-ref="userDataSource"/>中key的值必須要和靜態鍵值對照類中的值相同,如果有多個值,可以配置多個< entry>標籤。第二個property屬性配置預設的資料來源。
動態切換是資料來源
[java] view plain copy
- 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”的結果,本質類似於作業系統中的“生產者消費者”問題。因此當多使用者訪問時,多資料來源可能會導致系統性能下降的後果。