功能需求是公司要做一个大的运营平台:
1、运营平台有自身的数据库,维护用户、角色、菜单、部分以及权限等基本功能。
2、运营平台还需要提供其他不同服务(服务A,服务B)的后台运营,服务A、服务B的数据库是独立的。
所以,运营平台至少要连三个库:运营库,A库,B库,并且希望达到针对每个功能请求能够自动切换到对应的数据源(我最终实现是针对Service的方法级别进行切换的,也可以实现针对每个DAO层的方法进行切换。我们系统的功能是相互之间比较独立的)。
第一步:配置多数据源
1、定义数据源:
我采用的数据源是阿里的DruidDataSource(用DBCP也行,这个随便)。配置如下:
<!-- 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" /> <!-- 配置监控统计拦截的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" /> <!-- 配置监控统计拦截的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" /> <!-- 配置监控统计拦截的filters --> <property name="filters" value="config,mergeStat,wall,log4j2" /> <property name="connectionProperties" value="config.decrypt=true" /> </bean>
我配置了三个数据源:oPDataSource(运营平台本身的数据源),serverADataSource,serverBDataSource。
2、配置multipleDataSource
multipleDataSource相当于以上三个数据源的一个代理,真正与Spring/Mybatis相结合的时multipleDataSource,和单独配置的DataSource使用没有分别:
<!-- Spring整合Mybatis:配置multipleDatasource --> <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean"> <property name="dataSource" ref="multipleDataSource" /> <!-- 自动扫描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 动态实现 --> <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 对Dao 接口动态实现,需要知道接口在哪 --> <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> <!-- 事务管理配置multipleDataSource --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="multipleDataSource"></property> </bean>
了解了multipleDataSource所处的位置之后,接下来重点看下multipleDataSource怎么实现,配置文件如下:
<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>
实现的Java代码如下,不需要过多的解释,很一目了然:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * * @ClassName: MultipleDataSource * @Description: 配置多个数据源<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,实现抽象方法determineCurrentLookupKey,这个方法会在每次获得数据库连接Connection的时候之前,决定本次连接的数据源Datasource,可以看下Spring的代码就很清晰了:
/*获取连接*/ public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); /*此处的determineCurrentLookupKey为抽象接口,获取具体的数据源名称*/ 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实现的接口*/ protected abstract Object determineCurrentLookupKey();
第二步:每次请求(Service方法级别)动态切换数据源
实现思路是利用Spring的AOP思想,拦截每次的Service方法调用,然后根据方法的整体路径名,动态切换multipleDataSource中的数据的key。我们的项目,针对不同服务也即不同数据库的操作,是彼此之间互相独立的,不太建议在同一个service方法中调用不同的数据源,这样的话需要将动态判断是否需要切换的频次(AOP拦截的频次)放在DAO级别,也就是SQL级别。另外,还不方便进行事务管理。
我们来看动态切换数据源的AOP实现:
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; /** * 数据源切换AOP * * @author yuzhu.peng * @since 2018-01-15 */ @Aspect @Order(1) public class MultipleDataSourceInterceptor { /** * 拦截器对所有的业务实现类请求之前进行数据源切换 特别注意,由于用到了多数据源,Mapper的调用最好只在*ServiceImpl,不然调用到非默认数据源的表时,会报表不存在的异常 * * @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的设置为serverA数据源,否则默认为后台的数据源 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); } } /** * 当操作完成时,释放当前的数据源 如果不释放,频繁点击时会发生数据源冲突,本是另一个数据源的表,结果跑到另外一个数据源去,报表不存在 * * @param joinPoint * @throws Throwable */ @After("execution(* com.xxxx.service..*.*ServiceImpl.*(..))") public void removeDataSoruce(JoinPoint joinPoint) throws Throwable { MultipleDataSource.removeDataSourceKey(); } }
拦截所有的ServiceImpl方法,根据方法的全限定名去判断属于那个数据源的功能,然后选择相应的数据源,发放执行完后,释放当前的数据源。注意我用到了Spring的 @Order,注解,接下来会讲到,当定义多个AOP的时候,order是很有用的。
其他:
一开始项目中并没有引入事务,所以一切都OK,每次都能访问到正确的数据源,当加入SPring的事务管理后,不能动态切换数据源了(也好像是事务没有生效,反正是二者没有同时有效),后来发现原因是AOP的执行顺序问题,所以用到了上边提到的SPring的Order:
order越小,先被执行。至此,既可以动态切换数据源,又可以成功用事务(在同一个数据源)。