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

  • 相关阅读:
    zabbix系列 ~ mongo监控相关
    zabbix系列 ~ 自动监控多实例功能
    mysql 查询优化 ~ 多表查询改写思路
    MGR架构~高可用架构细节的梳理
    mysql 半同步复制~ 整体概述与改进
    android 单独编译某个模块
    Nginx使用
    21天学通C++学习笔记(九):类和对象
    1. 个人经验总结
    C++视频教程学习笔记
  • 原文地址:https://www.cnblogs.com/nietzsche2019/p/11305654.html
Copyright © 2011-2022 走看看