動態切換數據源理論知識
項目中我們經常會遇到多數據源的問題,尤其是數據同步或定時任務等項目更是如此;又例如:讀寫分離數據庫配置的系統。
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層里面動態切換數據源。
下面是我在實際項目中的一點應用(我是將事務控制和數據源切換都放在了service層,通過spring的aop設置先切換數據源再開啟事務控制),相關配置分享到這里,大家共同探討,歡迎技術交流(顯示“xx”部分根據自己項目填寫相應數據)
1、首先,要有數據庫的相關配置文件jdbc.properties:
jdbc.rmi.driverClassName = com.csw.common.log4jdbc.CswDriverSpy
jdbc.rmi.url1 = jdbc:log4jdbc:oracle:thin:@192.168.x.x:1521:xx
jdbc.rmi.user1 = xxxx
jdbc.rmi.password1 = ****
jdbc.rmi.url2 = jdbc:log4jdbc:oracle:thin:@192.168.x.x:1521:xx
jdbc.rmi.user2 = xxxx
jdbc.rmi.password2 = ****
2、用spring管理數據源
<bean id="dataSource1" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.rmi.driverClassName}"/> <property name="jdbcUrl" value="${jdbc.rmi.url1}"/> <property name="username" value="${jdbc.rmi.user1}"/> <property name="password" value="${jdbc.rmi.password1}"/> <property name="connectionTestQuery" value="SELECT 1 FROM DUAL"/> <property name="maximumPoolSize" value="xx"/> <property name="idleTimeout" value="xx"/> <property name="maxLifetime" value="xx"/> <property name="minimumIdle" value="xx"/> <property name="poolName" value="ScmDatabasePool"/> <property name="dataSourceProperties"> <props> <prop key="cachePrepStmts">true</prop> <prop key="prepStmtCacheSize">xx</prop> <prop key="prepStmtCacheSqlLimit">xx</prop> <prop key="useServerPrepStmts">true</prop> </props> </property> </bean> <bean id="dataSource2" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.rmi.driverClassName}"/> <property name="jdbcUrl" value="${jdbc.rmi.url2}"/> <property name="username" value="${jdbc.rmi.user2}"/> <property name="password" value="${jdbc.rmi.password2}"/> <property name="connectionTestQuery" value="SELECT 1 FROM DUAL"/> <property name="maximumPoolSize" value="xx"/> <property name="idleTimeout" value="xx"/> <property name="maxLifetime" value="xx"/> <property name="minimumIdle" value="xx"/> <property name="poolName" value="ScmDatabasePool"/> <property name="dataSourceProperties"> <props> <prop key="cachePrepStmts">true</prop> <prop key="prepStmtCacheSize">xx</prop> <prop key="prepStmtCacheSqlLimit">xx</prop> <prop key="useServerPrepStmts">true</prop> </props> </property> </bean>
3、上面的數據源配置起來了,但是怎么樣才能實現一個sessionFactory來管理兩個源呢,需要一個動態的代理類,寫一個RoutingDataSource類繼承 AbstractRoutingDataSource ,並實現 determineCurrentLookupKey方法即可,AbstractRoutingDataSource是spring里的一個實現類,有興趣的朋友可以研究一下他的源碼。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * @author * @version 2019-08-02 12:36 */ public class RoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceHolder.getDataSourceType(); } }
還要寫一個數據源持有類,利用ThreadLocal解決線程安全問題
/** * @author * @version 2019-08-02 13:12 */ public class DataSourceHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); /** * @Description: 設置數據源類型 * @param dataSourceType 數據庫類型 * @return void * @throws */ public static void setDataSourceType(String dataSourceType) { contextHolder.set(dataSourceType); } /** * @Description: 獲取數據源類型 * @param * @return String * @throws */ public static String getDataSourceType() { return contextHolder.get(); } /** * @Description: 清除數據源類型 * @param * @return void * @throws */ public static void clearDataSourceType() { contextHolder.remove(); } }
4、實現一個sessionFactory管理多個數據源
<bean id="dataSource" class="com.csw.purchase.config.RoutingDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <!--通過不同的key決定用哪個dataSource--> <entry key="ds1" value-ref="dataSource1"/> <entry key="ds2" value-ref="dataSource2"/> </map> </property> <!-- 為指定數據源RoutingDataSource注入默認的數據源--> <property name="defaultTargetDataSource" ref="dataSource1"/> </bean>
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configuration" ref="mybatisConfig"/> <property name="typeAliasesPackage" value="com.csw.*.entity"/> <property name="plugins"> <array> <bean id="paginationInterceptor" class="com.baomidou.mybatisplus.plugins.PaginationInterceptor"/> <bean id="optimisticLockerInterceptor" class="com.baomidou.mybatisplus.plugins.OptimisticLockerInterceptor"/> </array> </property> <property name="globalConfig" ref="globalConfig"/> </bean>
5、 建立一個數據源切面類,分別實現org.springframework.aop中的MethodBeforeAdvice、AfterReturningAdvice、ThrowsAdvice 三個接口,一開始我並未實現ThrowsAdvice 接口,后來在程序調試過程中發現數據源一旦切換到非默認數據源,目標方法(帶有其他數據源注解的方法)拋出異常后將導致數據源切換失敗,報talbe or view does not exist錯誤,究其原因應該是數據源持有類的DataSourceHolder中的線程ThreadLocal由於異常導致contextHolder.remove()未被執行,實現了ThrowsAdvice 接口后,可以完美解決這個問題,具體代碼如下:
import org.aspectj.lang.annotation.Aspect; import org.springframework.aop.AfterReturningAdvice; import org.springframework.aop.MethodBeforeAdvice; import org.springframework.aop.ThrowsAdvice; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * @author * @version 2019-08-02 13:15 */ @Aspect @Component public class DataSourceAspect implements MethodBeforeAdvice, AfterReturningAdvice, ThrowsAdvice { @Override public void afterReturning(Object o, Method method, Object[] objects, Object o1) { if(method.isAnnotationPresent(DataSource.class)) { DataSourceHolder.clearDataSourceType(); System.out.println("**********************************數據源已移除*************************************"); } } @Override public void before(final Method method, final Object[] args, final Object target) { if(method.isAnnotationPresent(DataSource.class)){ DataSource dataSource = method.getAnnotation(DataSource.class); DataSourceHolder.setDataSourceType(dataSource.value()); System.out.println("*******************************數據源切換至:"+DataSourceHolder.getDataSourceType()+"**************************************"); } } public void afterThrowing(final Method method, final Object[] args, final Object target, Exception e) { if(method.isAnnotationPresent(DataSource.class)) { DataSourceHolder.clearDataSourceType(); System.out.println("**********************************數據源已移除*************************************"); } } }
6、建立數據源注解類,不加數據源注解的方法使用默認數據源,加了注解的使用注解對應的數據源
import org.springframework.stereotype.Component; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author * @version 2019-08-02 13:14 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Component public @interface DataSource { String value() default ""; }
7、設置數據庫事務切面和切換數據庫切面執行的順序,利用aop的order屬性設置執行的順序,這樣實現了帶事務管理的spring數據庫動態切換
<aop:config> <aop:pointcut id="transactionPointcut" expression="execution(* com.csw.*.service.impl..*.*(..))"/> <aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" order="2"/> <aop:advisor pointcut-ref="transactionPointcut" advice-ref="dataSourceAspect" order="1"/> </aop:config>
8、測試,加了注解“ds2”的方法將用數據源ds2
@DataSource("ds2") public Page<SupplierServiceOrder> listSupplierServiceOrderQuery(final Page<SupplierServiceOrder> page, final SupplierServiceOrder supplierServiceOrder) { page.setRecords(baseMapper.listSupplierServiceOrderQuery(page, supplierServiceOrder)); return page; }
目前上述配置實現了單個service調用單個方法調用單個數據源的帶事務的數據源動態切換,如果該方法中需要調用另外的數據源,由於此時事務已經開啟,按上述方法應該會導致另外的數據源切換失敗,按上述配置,只能將此種情況按調用的數據源不同分開寫在兩個service方法中,然后再在controller層將結果合到一起。目前項目中暫未遇到這種情況,待遇到來驗證。