您好,登錄后才能下訂單哦!
功能需求是公司要做一個(gè)大的運(yùn)營平臺:
1、運(yùn)營平臺有自身的數(shù)據(jù)庫,維護(hù)用戶、角色、菜單、部分以及權(quán)限等基本功能。
2、運(yùn)營平臺還需要提供其他不同服務(wù)(服務(wù)A,服務(wù)B)的后臺運(yùn)營,服務(wù)A、服務(wù)B的數(shù)據(jù)庫是獨(dú)立的。
所以,運(yùn)營平臺至少要連三個(gè)庫:運(yùn)營庫,A庫,B庫,并且希望達(dá)到針對每個(gè)功能請求能夠自動(dòng)切換到對應(yīng)的數(shù)據(jù)源(我最終實(shí)現(xiàn)是針對Service的方法級別進(jìn)行切換的,也可以實(shí)現(xiàn)針對每個(gè)DAO層的方法進(jìn)行切換。我們系統(tǒng)的功能是相互之間比較獨(dú)立的)。
第一步:配置多數(shù)據(jù)源
1、定義數(shù)據(jù)源:
我采用的數(shù)據(jù)源是阿里的DruidDataSource(用DBCP也行,這個(gè)隨便)。配置如下:
<!-- op dataSource --> <bean id="opDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${db.master.url}" /> <property name="username" value="${db.master.user}" /> <property name="password" value="${db.master.password}" /> <property name="driverClassName" value="${db.master.driver}" /> <property name="initialSize" value="5" /> <property name="maxActive" value="100" /> <property name="minIdle" value="10" /> <property name="maxWait" value="60000" /> <property name="validationQuery" value="SELECT 'x'" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="600000" /> <property name="minEvictableIdleTimeMillis" value="300000" /> <property name="removeAbandoned" value="true" /> <property name="removeAbandonedTimeout" value="1800" /> <property name="logAbandoned" value="true" /> <!-- 配置監(jiān)控統(tǒng)計(jì)攔截的filters --> <property name="filters" value="config,mergeStat,wall,log4j2" /> <property name="connectionProperties" value="config.decrypt=true" /> </bean> <!-- serverA dataSource --> <bean id="serverADataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${db.serverA.master.url}" /> <property name="username" value="${db.serverA.master.user}" /> <property name="password" value="${db.serverA.master.password}" /> <property name="driverClassName" value="${db.serverA.master.driver}" /> <property name="initialSize" value="5" /> <property name="maxActive" value="100" /> <property name="minIdle" value="10" /> <property name="maxWait" value="60000" /> <property name="validationQuery" value="SELECT 'x'" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="600000" /> <property name="minEvictableIdleTimeMillis" value="300000" /> <property name="removeAbandoned" value="true" /> <property name="removeAbandonedTimeout" value="1800" /> <property name="logAbandoned" value="true" /> <!-- 配置監(jiān)控統(tǒng)計(jì)攔截的filters --> <property name="filters" value="config,mergeStat,wall,log4j2" /> <property name="connectionProperties" value="config.decrypt=true" /> </bean> <!-- serverB dataSource --> <bean id="serverBDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${db.serverB.master.url}" /> <property name="username" value="${db.serverB.master.user}" /> <property name="password" value="${db.serverB.master.password}" /> <property name="driverClassName" value="${db.serverB.master.driver}" /> <property name="initialSize" value="5" /> <property name="maxActive" value="100" /> <property name="minIdle" value="10" /> <property name="maxWait" value="60000" /> <property name="validationQuery" value="SELECT 'x'" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="600000" /> <property name="minEvictableIdleTimeMillis" value="300000" /> <property name="removeAbandoned" value="true" /> <property name="removeAbandonedTimeout" value="1800" /> <property name="logAbandoned" value="true" /> <!-- 配置監(jiān)控統(tǒng)計(jì)攔截的filters --> <property name="filters" value="config,mergeStat,wall,log4j2" /> <property name="connectionProperties" value="config.decrypt=true" /> </bean>
我配置了三個(gè)數(shù)據(jù)源:oPDataSource(運(yùn)營平臺本身的數(shù)據(jù)源),serverADataSource,serverBDataSource。
2、配置multipleDataSource
multipleDataSource相當(dāng)于以上三個(gè)數(shù)據(jù)源的一個(gè)代理,真正與Spring/Mybatis相結(jié)合的時(shí)multipleDataSource,和單獨(dú)配置的DataSource使用沒有分別:
<!-- Spring整合Mybatis:配置multipleDatasource --> <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean"> <property name="dataSource" ref="multipleDataSource" /> <!-- 自動(dòng)掃描Mapping.xml文件 --> <property name="mapperLocations"> <list> <value>classpath*:/sqlMapperXml/*.xml</value> <value>classpath*:/sqlMapperXml/*/*.xml</value> </list> </property> <property name="configLocation" value="classpath:xml/mybatis-config.xml"></property> <property name="typeAliasesPackage" value="com.XXX.platform.model" /> <property name="globalConfig" ref="globalConfig" /> <property name="plugins"> <array> <!-- 分頁插件配置 --> <bean id="paginationInterceptor" class="com.baomidou.mybatisplus.plugins.PaginationInterceptor"> <property name="dialectType" value="mysql" /> <property name="optimizeType" value="aliDruid" /> </bean> </array> </property> </bean> <!-- MyBatis 動(dòng)態(tài)實(shí)現(xiàn) --> <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 對Dao 接口動(dòng)態(tài)實(shí)現(xiàn),需要知道接口在哪 --> <property name="basePackage" value="com.XXX.platform.mapper" /> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> </bean> <!-- MP 全局配置 --> <bean id="globalConfig" class="com.baomidou.mybatisplus.entity.GlobalConfiguration"> <property name="idType" value="0" /> <property name="dbColumnUnderline" value="true" /> </bean> <!-- 事務(wù)管理配置multipleDataSource --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="multipleDataSource"></property> </bean>
了解了multipleDataSource所處的位置之后,接下來重點(diǎn)看下multipleDataSource怎么實(shí)現(xiàn),配置文件如下:
<bean id="multipleDataSource" class="com.xxxx.platform.commons.db.MultipleDataSource"> <property name="defaultTargetDataSource" ref="opDataSource" /> <property name="targetDataSources"> <map> <entry key="opDataSource" value-ref="opDataSource" /> <entry key="serverADataSource" value-ref="serverADataSource" /> <entry key="serverBDataSource" value-ref="serverBDataSource" /> </map> </property> </bean>
實(shí)現(xiàn)的Java代碼如下,不需要過多的解釋,很一目了然:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * * @ClassName: MultipleDataSource * @Description: 配置多個(gè)數(shù)據(jù)源<br> * @author: yuzhu.peng * @date: 2018年1月12日 下午4:37:25 */ public class MultipleDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<String>(); public static void setDataSourceKey(String dataSource) { dataSourceKey.set(dataSource); } @Override protected Object determineCurrentLookupKey() { return dataSourceKey.get(); } public static void removeDataSourceKey() { dataSourceKey.remove(); } }
繼承自spring的AbstractRoutingDataSource,實(shí)現(xiàn)抽象方法determineCurrentLookupKey,這個(gè)方法會(huì)在每次獲得數(shù)據(jù)庫連接Connection的時(shí)候之前,決定本次連接的數(shù)據(jù)源Datasource,可以看下Spring的代碼就很清晰了:
/*獲取連接*/ public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); /*此處的determineCurrentLookupKey為抽象接口,獲取具體的數(shù)據(jù)源名稱*/ Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); if ((dataSource == null) && (((this.lenientFallback) || (lookupKey == null)))) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; } /*抽象接口:也即我們的multipleDataSource實(shí)現(xiàn)的接口*/ protected abstract Object determineCurrentLookupKey();
第二步:每次請求(Service方法級別)動(dòng)態(tài)切換數(shù)據(jù)源
實(shí)現(xiàn)思路是利用Spring的AOP思想,攔截每次的Service方法調(diào)用,然后根據(jù)方法的整體路徑名,動(dòng)態(tài)切換multipleDataSource中的數(shù)據(jù)的key。我們的項(xiàng)目,針對不同服務(wù)也即不同數(shù)據(jù)庫的操作,是彼此之間互相獨(dú)立的,不太建議在同一個(gè)service方法中調(diào)用不同的數(shù)據(jù)源,這樣的話需要將動(dòng)態(tài)判斷是否需要切換的頻次(AOP攔截的頻次)放在DAO級別,也就是SQL級別。另外,還不方便進(jìn)行事務(wù)管理。
我們來看動(dòng)態(tài)切換數(shù)據(jù)源的AOP實(shí)現(xiàn):
import java.lang.reflect.Proxy; import org.apache.commons.lang.ClassUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; /** * 數(shù)據(jù)源切換AOP * * @author yuzhu.peng * @since 2018-01-15 */ @Aspect @Order(1) public class MultipleDataSourceInterceptor { /** * 攔截器對所有的業(yè)務(wù)實(shí)現(xiàn)類請求之前進(jìn)行數(shù)據(jù)源切換 特別注意,由于用到了多數(shù)據(jù)源,Mapper的調(diào)用最好只在*ServiceImpl,不然調(diào)用到非默認(rèn)數(shù)據(jù)源的表時(shí),會(huì)報(bào)表不存在的異常 * * @param joinPoint * @throws Throwable */ @Before("execution(* com.xxxx.platform.service..*.*ServiceImpl.*(..))") public void setDataSoruce(JoinPoint joinPoint) throws Throwable { Class<?> clazz = joinPoint.getTarget().getClass(); String className = clazz.getName(); if (ClassUtils.isAssignable(clazz, Proxy.class)) { className = joinPoint.getSignature().getDeclaringTypeName(); } // 對類名含有serverA的設(shè)置為serverA數(shù)據(jù)源,否則默認(rèn)為后臺的數(shù)據(jù)源 if (className.contains(".serverA.")) { MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_serverA); } else if (className.contains(".serverB.")) { MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_serverB); } else { MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_OP); } } /** * 當(dāng)操作完成時(shí),釋放當(dāng)前的數(shù)據(jù)源 如果不釋放,頻繁點(diǎn)擊時(shí)會(huì)發(fā)生數(shù)據(jù)源沖突,本是另一個(gè)數(shù)據(jù)源的表,結(jié)果跑到另外一個(gè)數(shù)據(jù)源去,報(bào)表不存在 * * @param joinPoint * @throws Throwable */ @After("execution(* com.xxxx.service..*.*ServiceImpl.*(..))") public void removeDataSoruce(JoinPoint joinPoint) throws Throwable { MultipleDataSource.removeDataSourceKey(); } }
攔截所有的ServiceImpl方法,根據(jù)方法的全限定名去判斷屬于那個(gè)數(shù)據(jù)源的功能,然后選擇相應(yīng)的數(shù)據(jù)源,發(fā)放執(zhí)行完后,釋放當(dāng)前的數(shù)據(jù)源。注意我用到了Spring的 @Order,注解,接下來會(huì)講到,當(dāng)定義多個(gè)AOP的時(shí)候,order是很有用的。
其他:
一開始項(xiàng)目中并沒有引入事務(wù),所以一切都OK,每次都能訪問到正確的數(shù)據(jù)源,當(dāng)加入SPring的事務(wù)管理后,不能動(dòng)態(tài)切換數(shù)據(jù)源了(也好像是事務(wù)沒有生效,反正是二者沒有同時(shí)有效),后來發(fā)現(xiàn)原因是AOP的執(zhí)行順序問題,所以用到了上邊提到的SPring的Order:
order越小,先被執(zhí)行。至此,既可以動(dòng)態(tài)切換數(shù)據(jù)源,又可以成功用事務(wù)(在同一個(gè)數(shù)據(jù)源)。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。