spring 動態數據源


1、動態數據源:

   在一個項目中,有時候需要用到多個數據庫,比如讀寫分離,數據庫的分布式存儲等等,這時我們要在項目中配置多個數據庫。

2、原理:

    (1)、spring 單數據源獲取數據連接過程:

    DataSource --> SessionFactory --> Session 
    DataSouce   實現javax.sql.DateSource接口的數據源, 
    DataSource  注入SessionFactory,
    從sessionFactory 獲取 Session,實現數據庫的 CRUD。

  (2)、動態數據源切換:  

    動態數據源原理之一:實現 javax.sql.DataSource接口, 封裝DataSource, 在 DataSource 配置多個數據庫連接,這種方式只需要一個dataSouce,就能實現多個數據源,最理想的實現,但是需要自己實現DataSource,自己實現連接池,對技術的要求較高,而且自己實現的連接池在性能和穩定性上都有待考驗。

    動態數據源原理之二:配置多個DataSource, SessionFactory注入多個DataSource,實現SessionFactory動態調用DataSource,這種方式需要自己實現SessesionFactory,第三方實現一般不支持注入多個DataSource。

    動態數據源原理之三:配置多個DataSource, 在DataSource和SessionFactory之間插入 RoutingDataSource路由,即 DataSource --> RoutingDataSource --> SessionFactory --> Session, 在SessionFactory調用時在 RoutingDataSource 層實現DataSource的動態切換, spring提供了 AbstratRoutingDataSource抽象類, 對動態數據源切換提供了很好的支持, 不需要開發者實現復雜的底層邏輯, 推薦實現方式。

    動態數據源原理之四:配置多個SessionFactory,這種實現對技術要求最低,但是相對切換數據源最不靈活。  

3、實現:

  這里我們使用原理三以讀寫分離為例,具體實現如下:

  步驟一:配置多個DateSource,使用的基於阿里的 DruidDataSource

<!-- 引入屬性文件,方便配置內容修改 -->
    <context:property-placeholder location="classpath:jdbc.properties" />
    
    
    <!-- 數據庫鏈接(主庫) -->
    <bean id="dataSourceRW" class="com.alibaba.druid.pool.DruidDataSource"
        destroy-method="close">
        <!-- 基本屬性 url、user、password -->
        <property name="url" value="${jdbc_url}" />
        <property name="username" value="${jdbc_username}" />
        <property name="password" value="${jdbc_password}" />

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${druid_initialSize}" />
        <property name="minIdle" value="${druid_minIdle}" />
        <property name="maxActive" value="${druid_maxActive}" />

        <!-- 配置獲取連接等待超時的時間 -->
        <property name="maxWait" value="${druid_maxWait}" />

        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testWhileIdle" value="true" />

        <!-- 打開PSCache,並且指定每個連接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
            value="100" />

        <!-- 密碼加密 -->
        <property name="filters" value="config" />
        <property name="connectionProperties" value="config.decrypt=true" />
    </bean>


        <!-- 數據庫鏈接(只讀庫) -->
    <bean id="dataSourceR" class="com.alibaba.druid.pool.DruidDataSource"
        destroy-method="close">
        <!-- 基本屬性 url、user、password -->
        <property name="url" value="${jdbc_url_read}" />
        <property name="username" value="${jdbc_username_read}" />
        <property name="password" value="${jdbc_password_read}" />

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${druid_initialSize}" />
        <property name="minIdle" value="${druid_minIdle}" />
        <property name="maxActive" value="${druid_maxActive}" />

        <!-- 配置獲取連接等待超時的時間 -->
        <property name="maxWait" value="${druid_maxWait}" />

        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testWhileIdle" value="true" />

        <!-- 打開PSCache,並且指定每個連接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
            value="100" />

        <!-- 密碼加密 -->
        <property name="filters" value="config" />
        <property name="connectionProperties" value="config.decrypt=true" />
    </bean>

步驟二:配置 DynamicDataSource

<!-- 動態數據源 -->  
   <bean id="dynamicDataSource" class="base.dataSource.DynamicDataSource">  
       <!-- 通過key-value關聯數據源 -->  
       <property name="targetDataSources">  
           <map>  
               <entry value-ref="dataSourceRW" key="dataSourceRW"></entry>  
               <entry value-ref="dataSourceR" key="dataSourceR"></entry>  
           </map>  
       </property>
       <!-- 默認的DataSource配置-->
       <property name="defaultTargetDataSource" ref="dataSourceR" />      
   </bean>
package base.dataSource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource{

    @Override
    protected Object determineCurrentLookupKey() {
        return DBContextHolder.getDbType();
    }
}

DynamicDataSource 繼承了spring 的 AbstractRoutingDataSource 抽象類 實現determineCurrentLookupKey()方法

  determineCurrentLookupKey()方法在 SessionFactory 獲取 DataSoure時被調用,AbstractRoutingDataSource 代碼:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.jdbc.datasource.lookup;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.util.Assert;

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    private Map<Object, DataSource> resolvedDataSources;
    private DataSource resolvedDefaultDataSource;

    public AbstractRoutingDataSource() {
    }

    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    public void setLenientFallback(boolean lenientFallback) {
        this.lenientFallback = lenientFallback;
    }

    public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
        this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null?dataSourceLookup:new JndiDataSourceLookup());
    }

    public void afterPropertiesSet() {
        if(this.targetDataSources == null) {
            throw new IllegalArgumentException("Property \'targetDataSources\' is required");
        } else {
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            Iterator var1 = this.targetDataSources.entrySet().iterator();

            while(var1.hasNext()) {
                Entry entry = (Entry)var1.next();
                Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
                DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
                this.resolvedDataSources.put(lookupKey, dataSource);
            }

            if(this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }

    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
        return lookupKey;
    }

    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
        if(dataSource instanceof DataSource) {
            return (DataSource)dataSource;
        } else if(dataSource instanceof String) {
            return this.dataSourceLookup.getDataSource((String)dataSource);
        } else {
            throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
        }
    }

    public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

    public Connection getConnection(String username, String password) throws SQLException {
        return this.determineTargetDataSource().getConnection(username, password);
    }

    public <T> T unwrap(Class<T> iface) throws SQLException {
        return iface.isInstance(this)?this:this.determineTargetDataSource().unwrap(iface);
    }

    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
    }

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.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 + "]");
        } else {
            return dataSource;
        }
    }

    protected abstract Object determineCurrentLookupKey();
}
AbstractRoutingDataSource 兩個主要變量:
  targetDataSources 初始化了 DataSource 的map集合, defaultTargetDataSource 初始化默認的DataSource 並實現了 DataSource的 getConnection() 獲取數據庫連接的方法,該方法從determineTargetDataSource()獲取  DataSource, determineTargetDataSource() 調用了我們 DynamicDataSource 中實現的 determineCurrentLookupKey() 方法獲取DataSource(determineCurrentLookupKey()方法返回的只是我們初始化的DataSource Map集合key值, 通過key獲取DataSource的方法這里不做贅述,感興趣自己研究下),
determineTargetDataSource()的主要邏輯是獲取我們切換的DataSource, 如果沒有的話讀取默認的DataSource。

在DynamicDataSource中我們定義了一個線程變量DBContextHolder來存放我們切換的DataSource, 防止其它線程覆蓋我們的DataSource。
package base.dataSource;

/**
 * 
 * @author xiao
 * @date 下午3:27:52
 */
public final class DBContextHolder {
    
    /** 
     * 線程threadlocal 
     */  
    private static ThreadLocal<String> contextHolder = new ThreadLocal<>();  
  
    private static String DEFAUL_DB_TYPE_RW = "dataSourceKeyRW";     
    
    /**
     * 獲取本線程的dbtype
     * @return
     */
    public static String getDbType() {  
        String db = contextHolder.get();  
        if (db == null) {  
            db = DEFAUL_DB_TYPE_RW;// 默認是讀寫庫  
        }  
        return db;  
    }  
  
    /** 
     *  
     * 設置本線程的dbtype 
     *  
     * @param str 
     */  
    public static void setDbType(String str) {  
        contextHolder.set(str);  
    }  
  
    /** 
     * clearDBType 
     *  
     * @Title: clearDBType 
     * @Description: 清理連接類型 
     */  
    public static void clearDBType() {  
        contextHolder.remove();  
    }  
}

    至此我們獲取DataSource的邏輯已完成, 接下來我們要考慮 設置DataSource, 即為DBContextHolder, set值。我們在代碼中調用DBContextHolder.set()來設置DataSource,理論上可以在代碼的任何位置設置, 不過為了統一規范,我們通過aop來實現,此時我們面臨的問題,在哪一層切入, 方案一: 在dao層切入,dao封裝了數據庫的CRUD,在這一層切入控制最靈活,但是我們一般在service業務層切入事務,如果在dao層切換數據源,會遇到事務無法同步的問題,雖然有分布式事務機制,但是目前成熟的框架很難用,如果使用過 就會知道分布式事務是一件非常惡心的事情,而且分布式事務本就不是一個好的選擇。方案二: 在service業務層切入,可以避免事務問題,但也相對影響了數據源切換的靈活性,這里要根據實際情況靈活選擇,我們采用的在service業務層切入,具體實現如下:

步驟三:實現aop

package base.dataSource.aop;

import java.util.Map;

import org.aspectj.lang.JoinPoint;
import org.springframework.core.Ordered;

import base.dataSource.DBContextHolder;

/**
 * 動態數據源切換aop
 * @author xiao
 * @date 2015年7月23日下午4:17:13
 */
public final class DynamicDataSourceAOP implements Ordered{


    /**
     * 方法, 數據源應映射規則map
     */
    Map<String, String> methods;

    /**
     * 默認數據源
     */
    String defaultDataSource;


    public String getDefaultDataSource() {
        return defaultDataSource;
    }

    public void setDefaultDataSource(String defaultDataSource) {
        if(null == defaultDataSource || "".equals(defaultDataSource)){
            throw new NullPointerException("defaultDataSource Must have a default value");
        }
        this.defaultDataSource = defaultDataSource;
    }

    public Map<String, String> getMethods() {
        return methods;
    }

    public void setMethods(Map<String, String> methods) {
        this.methods = methods;
    }

    /**
     * before 數據源切換
     *
     * @param pjp
     * @throws Throwable
     */
    public void dynamicDataSource(JoinPoint pjp) throws Throwable {
        DBContextHolder.setDbType(getDBTypeKey(pjp.getSignature().getName()));
    }

    private String getDBTypeKey(String methodName) {
        methodName = methodName.toUpperCase();
        for (String method : methods.keySet()) {
            String m = method.toUpperCase();
            /**
             * 忽略大小寫
             * method 如果不包含 '*', 則以方法名匹配 method
             * method 包含 '*', 則匹配以 method 開頭, 或者 等於method 的方法
             */
            if (!method.contains("*")
                    && m.equals(methodName)
                    || methodName
                    .startsWith(m.substring(0, m.indexOf("*") - 1))
                    || methodName.equals(m.substring(0, m.indexOf("*") - 1))) {
                return methods.get(method);
            }
        }
        return defaultDataSource;
    }

    //設置AOP執行順序, 這里設置優於事務
    @Override
    public int getOrder() {
        return 1;
    }
}

   這里有一個小知識點,aop實現類實現了orderd接口,這個接口有一個方法getOrder(),返回aop的執行順序,就是在同一個切點如果切入了多個aop,則按order從小到大執行,這里我們設置優於事務aop,因為事務是 基於dataSource的,即先切換數據源,在開啟事務,否則可能會存在切換了已開啟了事務的數據源,導致事務不生效。

步驟四:配置aop切面

<!-- 數據源讀寫分離  aop -->
    <bean id="dynamicDataSourceAOP" class="base.dataSource.aop.DynamicDataSourceAOP">
        <property name="methods"> 
             <map>                  
                 <entry key="select*" value="dataSourceKeyR" />
                 <entry key="get*" value="dataSourceKeyR" />
                 <entry key="find*" value="dataSourceKeyR" />
                 <entry key="page*" value="dataSourceKeyR" />            
                 <entry key="query*" value="dataSourceKeyRW" />
             </map>
           </property>
        <property name="defaultDataSource" value="dataSourceKeyRW"/>
       </bean>
       
       
    <aop:config>
        <!-- 切點 管理所有Service的方法 -->
        <aop:pointcut
            expression="execution(* com.b2c.*.service.*Service.*(..))"
            id="transactionPointCut" />                
        <!-- 進行事務控制 Advisor -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointCut" />
        
        <!-- 動態數據源aop,  aop:advisor配置一定要在  aop:aspect之前,否則報錯    -->
        <aop:aspect ref="dynamicDataSourceAOP">            
            <aop:before method="dynamicDataSource" pointcut-ref="transactionPointCut" />        
        </aop:aspect>
        
    </aop:config>

至此全部完成, 另外這只是個人觀點,有更好的想法歡迎交流指正。

 


免責聲明!

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



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