带事务管理的spring数据库动态切换
动态切换数据源理论知识
项目中我们经常会遇到多数据源的问题,尤其是数据同步或定时任务等项目更是如此;又例如:读写分离数据库配置的系统。
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层将结果合到一起。目前项目中暂未遇到这种情况,待遇到来验证。