帶事務管理的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層將結果合到一起。目前項目中暫未遇到這種情況,待遇到來驗證。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM