spring 動態切換資料來源 多資料庫__資料庫

來源:互聯網
上載者:User
1.背景 對於資料量在1千萬,單個mysql資料庫就可以支援,但是如果資料量大於這個數的時候,例如1億,那麼查詢的效能就會很低。此時需要對資料庫做水平切分,常見的做法是按照使用者的帳號進行hash,然後選擇對應的資料庫。 水平切分圖,資料落入不同的庫中
2.實現 2.1示意圖

先來看下大致示意圖:

- 圖1是比較常見的情況,單個資料庫
- 圖2展示了web應用和資料庫之間的一個中介層,這個中介層去選擇使用哪個資料庫。 2.2資料庫配置 首先我們需要配置多個資料來源,我是用xml進行配置的其他方法大同小異,就是多建立了幾個bean。

    <bean id="parentDataSource" abstract="true"          class="org.apache.tomcat.jdbc.pool.DataSource"          destroy-method="close"          p:maxWait="10000"          p:removeAbandoned="true"          p:removeAbandonedTimeout="180"          p:connectionProperties="clientEncoding=UTF-8"          p:validationQuery="SELECT 1"          p:validationInterval="30000"          p:testOnBorrow="false"          p:testOnReturn="false"          p:testWhileIdle="true"          p:timeBetweenEvictionRunsMillis="10000"          p:minEvictableIdleTimeMillis="60000"          p:logAbandoned="false"          p:defaultAutoCommit="true" />    <bean id="dataSource" parent="parentDataSource"          p:driverClassName="com.mysql.jdbc.Driver"          p:username="${jdbc.user}"          p:password="${jdbc.password}"          p:initialSize="20"          p:maxActive="200"          p:maxIdle="200"          p:minIdle="5"/>    <bean id="childDataSource1" parent="dataSource">        <property name="url" value="${jdbc.url1}" />    </bean>    <bean id="childDataSource2" parent="dataSource">        <property name="url" value="${jdbc.url2}" />    </bean>
這裡我建了兩個資料來源bean的id分別為childDataSource1,childDataSource2,為了方便起見,這裡只有資料庫的url不同,故都繼承了dataSource。 2.3 java實現

先把定義的多個資料庫bean放一放,先來看下spring中對動態選擇資料來源的支援。 在spring中有一個抽象類別AbstractRoutingDataSource類,通過這個類可以實現動態選擇資料來源。來看下這個類的成員變數

    private Map<Object, Object> targetDataSources;    private Object defaultTargetDataSource;    private Map<Object, DataSource> resolvedDataSources;
targetDataSources中儲存了key和資料庫連接的映射關係,defaultTargetDataSource表示預設的連結,resolvedDataSources這個資料結構是通過targetDataSources構建而來,儲存的結構也是資料庫標識和資料來源的映射關係。 下面需要繼承AbstractRoutingDataSource類,實現我們自己的資料庫選擇邏輯DataSourceSwitcher類,先上代碼:
public class DataSourceSwitcher extends AbstractRoutingDataSource{    private static final Logger LOGGER = LoggerFactory.getLogger("INTERACTIVE_LOGGER");    private static final ThreadLocal<String> dataSourceKey = new ThreadLocal<String>();    public static void clearDataSourceType() {        LOGGER.debug("thread:{},remove,dataSource:{}",Thread.currentThread().getName());        dataSourceKey.remove();    }    @Override    protected Object determineCurrentLookupKey() {        String s = dataSourceKey.get();        LOGGER.debug("thread:{},determine,dataSource:{}",Thread.currentThread().getName(),s);        return s;    }    public static void setDataSourceKey(String dataSource) {        LOGGER.debug("thread:{},set,dataSource:{}",Thread.currentThread().getName(),dataSource);        dataSourceKey.set(dataSource);    }}
第5行,threadLocal的成員變數dataSource(由於不同的請求所需要的資料來源可能不一樣),用於儲存資料來源標識。 第8行,清除資料來源操作. 第14行,該方法決定了需要使用哪個資料庫,該方法是抽象方法,必須由我們實現,那麼現在來看下這個方法是如何使用的
    protected DataSource determineTargetDataSource() {        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");        Object lookupKey = determineCurrentLookupKey();//這裡擷取資料庫標識        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 + "]");        }        return dataSource;    }

所以我們需要在determineCurrentLookupKey方法中返回資料庫標識即可 第20行,設定資料來源方法。 2.4 資料庫配置和DataSourceSwitcher類結合 現在把我們之前的資料庫配置和DataSourceSwitcher進行合并,我在資料庫的xml配置上添加如下配置:

    <bean id="dataSourceSwitcher" class="com.netease.mail.activity.service.switcher.DataSourceSwitcher">        <property name="targetDataSources">            <map>                <entry key="ds1" value-ref="childDataSource1"/>                <entry key="ds2" value-ref="childDataSource2"/>            </map>        </property>        <property name="defaultTargetDataSource" ref="childDataSource1"/>    </bean>    <bean id="transactionManager"          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">        <property name="dataSource" ref="dataSourceSwitcher" />    </bean>    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">        <property name="configLocation" value="classpath:mybatis/sql-map-config.xml" />        <property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/>        <property name="dataSource" ref="dataSourceSwitcher" />    </bean>
可以看到,我對targetDataSources進行了初始化,ds1對應了資料來源childDataSource1;ds2對應了資料來源childDataSource2。 使用的話只要調用 DataSourceSwitcher.setDataSourceKey(“ds1”),即將資料來源切換到了childDataSource12.5 增加切面處理 如果每次執行方法都要設定一下資料來源實在是件很麻煩的事情,另外我們需要對某個key進行hash後選擇資料庫,這塊也沒有實現。現在藉助spring切面的功能,可以解決這兩個問題。 大致思路如下:
2.5.1 自訂註解 定義UseDataSource註解
/** * 資料來源註解 * Created by hzlaojiaqi on 2017/12/26. */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface UseDataSource {     /**      * 資料來源      * @return      */     DataSourceType value() default DataSourceType.SOURCE_1;     /**      * 是否使用hashkey,若為true,則使用對應欄位的雜湊值進行計算,選擇資料來源,      * 且指定的{@link DataSourceType}不起作用      * @return      */     boolean useHashKey() default false;}
DataSourceType為枚舉類型,如下
@Getterpublic enum DataSourceType {    SOURCE_1("ds1", "資料來源1-預設資料來源"),    SOURCE_2("ds2", "資料來源2");    DataSourceType(String source, String desc) {        this.source = source;        this.desc = desc;    }    String source;    String desc;    /*     * @param hashKey     * @return     */    public static String getByKey(String hashKey){        //根據hashkey來擷取所需要的資料來源        int i = Math.abs(hashKey.hashCode()) % DataSourceType.values().length;        return DataSourceType.values()[i].getSource();    }}
useHashKey是否使用hashkey。 定義DSKey註解,該註解用於標註在對應的方法變數上,表示對該變數的值進行hash。
/** * * 資料來源選擇 註解 * 用在參數上,表示使用對應欄位的hashcode來選擇資料庫 * Created by hzlaojiaqi on 2017/12/26. */@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface DSKey {    String value() default "";}
2.5.2 aop進行攔截 由於只需要對UseDataSource這個註解進行攔截,因此切點可以設定如下
   @Pointcut("@annotation(com.netease.mail.activity.aop.annotation.UseDataSource)")    public void useDataSource() {    } 

流程圖如下所示

切點處理Around方法如下:

    /**     * @param joinPoint     * @return     * @throws Throwable     */    @Around("useDataSource() && @annotation(anno)")    public Object dataSourceSwitcher(ProceedingJoinPoint joinPoint, UseDataSource anno) throws Throwable {        String ds="";        //若使用hashkey,則根據hashkey進行選擇資料來源        if(anno.useHashKey()){            ds=DataSourceType.getByKey(getHashKeyFromMethod(joinPoint));        }else{            //直接擷取資料來源            DataSourceType value = anno.value();            ds=value.getSource();        }        //設定資料來源        DataSourceSwitcher.setDataSourceKey(ds);        try {            //執行方法            Object result = joinPoint.proceed();            return result;        }catch (Exception e){            throw e;        }finally {            //切換回原來的資料來源(重要)              DataSourceSwitcher.setDataSourceKey(DataSourceType.SOURCE_1.getSource());        }    }
getHashKeyFromMethod方法擷取了用@DSKey標註的變數的值,實現如下:
    /**     * @param joinPoint     * @return     */    public String getHashKeyFromMethod(ProceedingJoinPoint joinPoint){        MethodSignature signature=MethodSignature.class.cast(joinPoint.getSignature());        Method method = signature.getMethod();        Object[] args = joinPoint.getArgs();        Parameter[] declaredFields = method.getParameters();        int index=0;        for(Parameter temp:declaredFields){            Annotation[] annotations = temp.getAnnotations();            for(Annotation anTemp:annotations){                if(anTemp instanceof DSKey){                    return String.valueOf(args[index]);                }            }            index++;        }        throw new RuntimeException("can not get field with @DsKey annotation");    }
3.使用 我們看下實際的效果,定義兩個controller, insert用的是固定的Dasrouce.SOURCE_1,insert2用的是uid的hash值進行資料來源的選擇。
    @RequestMapping(value = "/ajax/insert.do",method = RequestMethod.GET)    @ResponseBody    @UseDataSource(DataSourceType.SOURCE_1)    public AjaxResult insert(@RequestParam String uid, HttpServletRequest httpServletRequest){        WebCouponWinner webCouponWinner=new WebCouponWinner();        webCouponWinner.setUid(uid);        webCouponWinner.setInsertTime(TimeUtil.now());        webCouponWinnerDao.insert(webCouponWinner);        return new AjaxResult(RetCode.SUCCESS);    }    @RequestMapping(value = "/ajax/insert2.do",method = RequestMethod.GET)    @ResponseBody    @UseDataSource(useHashKey = true)    public AjaxResult insert2(@RequestParam @DSKey String uid, HttpServletRequest httpServletRequest){        WebCouponWinner webCouponWinner=new WebCouponWinner();        webCouponWinner.setUid(uid);        webCouponWinner.setInsertTime(TimeUtil.now());        webCouponWinnerDao.insert(webCouponWinner);        return new AjaxResult(RetCode.SUCCESS);    }

執行/ajax/insert.do,uid分別傳1和2,得到如下結果

兩條記錄都在同一個資料庫中

再執行/ajax/insert2.do,uid分別傳1和2,得到如下結果

以及

兩條記錄在不同的庫中,符合預期。

完整的DataSourceAsp,需要注意下該切面必須要在事務註解@Transactional之前,由於在開始事務之前就需要確定資料來源,所以設定DataSourceAsp的@Order(Ordered.LOWEST_PRECEDENCE-1),@Transactional的order是最小值

package com.netease.mail.activity.aop;import com.netease.mail.activity.aop.annotation.DSKey;import com.netease.mail.activity.aop.annotation.UseDataSource;import com.netease.mail.activity.aop.type.DataSourceType;import com.netease.mail.activity.exception.custom.BizException;import com.netease.mail.activity.service.complex.MonitorService;import com.netease.mail.activity.service.switcher.DataSourceSwitcher;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.Ordered;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;import java.lang.annotation.Annotation;import java.lang.reflect.Method;import java.lang.reflect.Parameter;/** * * 資料來源切換 * Created by hzlaojiaqi on 2017/12/26. */@Component@Aspect@Slf4j(topic = "THIRDPARTY_LOGGER")@Order(Ordered.LOWEST_PRECEDENCE-1)public class DataSourceAsp {    @Autowired    MonitorService mMonitor;    /**     * 針對所有的Mapped     */    @Pointcut("@annotation(com.netease.mail.activity.aop.annotation.UseDataSource)")    public void useDataSource() {    }    /**     * @param joinPoint     * @return     * @throws Throwable     */    @Around("useDataSource() && @annotation(anno)")    public Object dataSourceSwitcher(ProceedingJoinPoint joinPoint, UseDataSource anno) throws Throwable {        String ds="";        if(anno.useHashKey()){            ds=DataSourceType.getByKey(getHashKeyFromMethod(joinPoint));        }else{            DataSourceType value = anno.value();            ds=value.getSource();        }        DataSourceSwitcher.setDataSourceKey(ds);        try {            Object result = joinPoint.proceed();            return result;        }catch (Exception e){            throw e;        }finally {            DataSourceSwitcher.setDataSourceKey(DataSourceType.SOURCE_1.getSource());        }    }    /**     * @param joinPoint     * @return     */    public String getHashKeyFromMethod(ProceedingJoinPoint joinPoint){        MethodSignature signature=MethodSignature.class.cast(joinPoint.getSignature());        Method method = signature.getMethod();        Object[] args = joinPoint.getArgs();        Parameter[] declaredFields = method.getParameters();        int index=0;        for(Parameter temp:declaredFields){            Annotation[] annotations = temp.getAnnotations();            for(Annotation anTemp:annotations){                if(anTemp instanceof DSKey){                    return String.valueOf(args[index]);                }            }            index++;        }        throw new BizException("can not get field with @DsKey annotation");    }}

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.