带事务管理的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层将结果合到一起。目前项目中暂未遇到这种情况,待遇到来验证。

posted @ 2019-08-05 21:31  敲代码的尼采  阅读(1645)  评论(0编辑  收藏  举报