使用Spring基於應用程式層實現讀寫分離

來源:互聯網
上載者:User
背景

我們一般應用對資料庫而言都是“讀多寫少”,也就說對資料庫讀取資料的壓力比較大,有一個思路就是說採用資料庫叢集的方案,
其中一個是主庫,負責寫入資料,我們稱之為:寫庫;
其它都是從庫,負責讀取資料,我們稱之為:讀庫;

那麼,對我們的要求是:
1、 讀庫和寫庫的資料一致;
2、 寫資料必須寫到寫庫;
3、 讀資料必須到讀庫; 方案

解決讀寫分離的方案有兩種:應用程式層解決和中介軟體解決。 應用程式層解決

優點:
1、 多資料來源切換方便,由程式自動完成;
2、 不需要引入中介軟體;
3、 理論上支援任何資料庫;
缺點:
1、 由程式員完成,營運參與不到;
2、 不能做到動態增加資料來源; 中介軟體解決

優點:
1、 來源程式不需要做任何改動就可以實現讀寫分離;
2、 動態添加資料來源不需要重啟程式;

缺點:
1、 程式依賴於中介軟體,會導致切換資料庫變得困難;
2、 由中介軟體做了中轉代理,效能有所下降;

相關中介軟體產品使用:
mysql-proxy:http://hi.baidu.com/geshuai2008/item/0ded5389c685645f850fab07
Amoeba for MySQL:http://www.iteye.com/topic/188598和http://www.iteye.com/topic/1113437 使用Spring基於應用程式層實現 原理

在進入Service之前,使用AOP來做出判斷,是使用寫庫還是讀庫,判斷依據可以根據方法名判斷,比如說以query、find、get等開頭的就走讀庫,其他的走寫庫。 代碼實現

DynamicDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;/** * 定義動態資料源,實現通過整合Spring提供的AbstractRoutingDataSource,只需要實現determineCurrentLookupKey方法即可 *  * 由於DynamicDataSource是單例的,線程不安全的,所以採用ThreadLocal保證安全執行緒,由DynamicDataSourceHolder完成。 *  * @author * */public class DynamicDataSource extends AbstractRoutingDataSource{    @Override    protected Object determineCurrentLookupKey() {        // 使用DynamicDataSourceHolder保證安全執行緒,並且得到當前線程中的資料來源key        return DynamicDataSourceHolder.getDataSourceKey();    }}

DynamicDataSourceHolder

/** *  * 使用ThreadLocal技術來記錄當前線程中的資料來源的key *  * @author * */public class DynamicDataSourceHolder {    //寫庫對應的資料來源key    private static final String MASTER = "master";    //讀庫對應的資料來源key    private static final String SLAVE = "slave";    //使用ThreadLocal記錄當前線程的資料來源key    private static final ThreadLocal<String> holder = new ThreadLocal<String>();    /**     * 設定資料來源key     * @param key     */    public static void putDataSourceKey(String key) {        holder.set(key);    }    /**     * 擷取資料來源key     * @return     */    public static String getDataSourceKey() {        return holder.get();    }    /**     * 標記寫庫     */    public static void markMaster(){        putDataSourceKey(MASTER);    }    /**     * 標記讀庫     */    public static void markSlave(){        putDataSourceKey(SLAVE);    }}

DataSourceAspect

import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.JoinPoint;/** * 定義資料來源的AOP切面,通過該Service的方法名判斷是應該走讀庫還是寫庫 *  * @author * */public class DataSourceAspect {    /**     * 在進入Service方法之前執行     *      * @param point 切面對象     */    public void before(JoinPoint point) {        // 擷取到當前執行的方法名        String methodName = point.getSignature().getName();        if (isSlave(methodName)) {            // 標記為讀庫            DynamicDataSourceHolder.markSlave();        } else {            // 標記為寫庫            DynamicDataSourceHolder.markMaster();        }    }    /**     * 判斷是否為讀庫     *      * @param methodName     * @return     */    private Boolean isSlave(String methodName) {        // 方法名以query、find、get開頭的方法名走從庫        return StringUtils.startsWithAny(methodName, "query", "find", "get");    }}
配置實現

jdbc.properties配置2個資料來源

jdbc.master.driver=com.mysql.jdbc.Driverjdbc.master.url=jdbc:mysql://127.0.0.1:3306/mybatis_1128?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=truejdbc.master.username=rootjdbc.master.password=123456jdbc.slave01.driver=com.mysql.jdbc.Driverjdbc.slave01.url=jdbc:mysql://127.0.0.1:3307/mybatis_1128?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=truejdbc.slave01.username=rootjdbc.slave01.password=123456

定義串連池

<!-- 配置串連池 -->    <bean id="masterDataSource" class="com.jolbox.bonecp.BoneCPDataSource"        destroy-method="close">        <!-- 資料庫驅動 -->        <property name="driverClass" value="${jdbc.master.driver}" />        <!-- 相應驅動的jdbcUrl -->        <property name="jdbcUrl" value="${jdbc.master.url}" />        <!-- 資料庫的使用者名稱 -->        <property name="username" value="${jdbc.master.username}" />        <!-- 資料庫的密碼 -->        <property name="password" value="${jdbc.master.password}" />        <!-- 檢查資料庫連接池中空閑串連的間隔時間,單位是分,預設值:240,如果要取消則設定為0 -->        <property name="idleConnectionTestPeriod" value="60" />        <!-- 串連池中未使用的連結最大存活時間,單位是分,預設值:60,如果要永遠存活設定為0 -->        <property name="idleMaxAge" value="30" />        <!-- 每個分區最大的串連數 -->        <property name="maxConnectionsPerPartition" value="150" />        <!-- 每個分區最小的串連數 -->        <property name="minConnectionsPerPartition" value="5" />    </bean>    <!-- 配置串連池 -->    <bean id="slave01DataSource" class="com.jolbox.bonecp.BoneCPDataSource"        destroy-method="close">        <!-- 資料庫驅動 -->        <property name="driverClass" value="${jdbc.slave01.driver}" />        <!-- 相應驅動的jdbcUrl -->        <property name="jdbcUrl" value="${jdbc.slave01.url}" />        <!-- 資料庫的使用者名稱 -->        <property name="username" value="${jdbc.slave01.username}" />        <!-- 資料庫的密碼 -->        <property name="password" value="${jdbc.slave01.password}" />        <!-- 檢查資料庫連接池中空閑串連的間隔時間,單位是分,預設值:240,如果要取消則設定為0 -->        <property name="idleConnectionTestPeriod" value="60" />        <!-- 串連池中未使用的連結最大存活時間,單位是分,預設值:60,如果要永遠存活設定為0 -->        <property name="idleMaxAge" value="30" />        <!-- 每個分區最大的串連數 -->        <property name="maxConnectionsPerPartition" value="150" />        <!-- 每個分區最小的串連數 -->        <property name="minConnectionsPerPartition" value="5" />    </bean>

定義DataSource

    <!-- 定義資料來源,使用自己實現的資料來源 -->    <bean id="dataSource" class="cn.itcast.usermanage.spring.DynamicDataSource">        <!-- 設定多個資料來源 -->        <property name="targetDataSources">            <map key-type="java.lang.String">                <!-- 這個key需要和程式中的key一致 -->                <entry key="master" value-ref="masterDataSource"/>                <entry key="slave" value-ref="slave01DataSource"/>            </map>        </property>        <!-- 設定預設的資料來源,這裡預設走寫庫 -->        <property name="defaultTargetDataSource" ref="masterDataSource"/>    </bean>

配置交易管理以及動態切換資料來源切面
1、定義交易管理員

    <!-- 定義交易管理員 -->    <bean id="transactionManager"        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">        <property name="dataSource" ref="dataSource" />    </bean>

2、定義事務策略

    <!-- 定義事務策略 -->    <tx:advice id="txAdvice" transaction-manager="transactionManager">        <tx:attributes>            <!--定義查詢方法都是唯讀 -->            <tx:method name="query*" read-only="true" />            <tx:method name="find*" read-only="true" />            <tx:method name="get*" read-only="true" />            <!-- 主庫執行操作,事務傳播行為定義為預設行為 -->            <tx:method name="save*" propagation="REQUIRED" />            <tx:method name="update*" propagation="REQUIRED" />            <tx:method name="delete*" propagation="REQUIRED" />            <!--其他方法使用預設事務策略 -->            <tx:method name="*" />        </tx:attributes>    </tx:advice>

3、定義切面

    <!-- 定義AOP切面處理器 -->    <bean class="cn.itcast.usermanage.spring.DataSourceAspect" id="dataSourceAspect" />    <aop:config>        <!-- 定義切面,所有的service的所有方法 -->        <aop:pointcut id="txPointcut" expression="execution(* xx.xxx.xxxxxxx.service.*.*(..))" />        <!-- 應用事務策略到Service切面 -->        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>        <!-- 將切面應用到自訂的切面處理器上,-9999保證該切面優先順序最高執行 -->        <aop:aspect ref="dataSourceAspect" order="-9999">            <aop:before method="before" pointcut-ref="txPointcut" />        </aop:aspect>    </aop:config>
提高篇1:改進切面實現,使用事務策略規則匹配

之前的實現我們是將通過方法名匹配,而不是使用事務策略中的定義,我們使用交易管理策略中的規則匹配。

改進後的配置

    <!-- 定義AOP切面處理器 -->    <bean class="cn.itcast.usermanage.spring.DataSourceAspect" id="dataSourceAspect">        <!-- 指定事務策略 -->        <property name="txAdvice" ref="txAdvice"/>        <!-- 指定slave方法的首碼(非必須) -->        <property name="slaveMethodStart" value="query,find,get"/>    </bean>

改進後的實現

import java.lang.reflect.Field;import java.util.ArrayList;import java.util.List;import java.util.Map;import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.JoinPoint;import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;import org.springframework.transaction.interceptor.TransactionAttribute;import org.springframework.transaction.interceptor.TransactionAttributeSource;import org.springframework.transaction.interceptor.TransactionInterceptor;import org.springframework.util.PatternMatchUtils;import org.springframework.util.ReflectionUtils;/** * 定義資料來源的AOP切面,該類控制了使用Master還是Slave。 *  * 如果交易管理中配置了事務策略,則採用配置的事務策略中的標記了ReadOnly的方法是用Slave,其它使用Master。 *  * 如果沒有配置交易管理的策略,則採用方法名匹配的原則,以query、find、get開頭方法用Slave,其它用Master。 *  * @author * */public class DataSourceAspect {    private List<String> slaveMethodPattern = new ArrayList<String>();    private static final String[] defaultSlaveMethodStart = new String[]{ "query", "find", "get" };    private String[] slaveMethodStart;    /**     * 讀取交易管理中的策略     *      * @param txAdvice     * @throws Exception     */    @SuppressWarnings("unchecked")    public void setTxAdvice(TransactionInterceptor txAdvice) throws Exception {        if (txAdvice == null) {            // 沒有配置交易管理策略            return;        }        //從txAdvice擷取到策略配置資訊        TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource();        if (!(transactionAttributeSource instanceof NameMatchTransactionAttributeSource)) {            return;        }        //使用反射技術擷取到NameMatchTransactionAttributeSource對象中的nameMap屬性值        NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource;        Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");        nameMapField.setAccessible(true); //設定該欄位可訪問        //擷取nameMap的值        Map<String, TransactionAttribute> map = (Map<String, TransactionAttribute>) nameMapField.get(matchTransactionAttributeSource);        //遍曆nameMap        for (Map.Entry<String, TransactionAttribute> entry : map.entrySet()) {            if (!entry.getValue().isReadOnly()) {//判斷之後定義了ReadOnly的策略才加入到slaveMethodPattern                continue;            }            slaveMethodPattern.add(entry.getKey());        }    }    /**     * 在進入Service方法之前執行     *      * @param point 切面對象     */    public void before(JoinPoint point) {        // 擷取到當前執行的方法名        String methodName = point.getSignature().getName();        boolean isSlave = false;        if (slaveMethodPattern.isEmpty()) {            // 當前Spring容器中沒有配置事務策略,採用方法名匹配方式            isSlave = isSlave(methodName);        } else {            // 使用原則規則匹配            for (String mappedName : slaveMethodPattern) {                if (isMatch(methodName, mappedName)) {                    isSlave = true;                    break;                }            }        }        if (isSlave) {            // 標記為讀庫            DynamicDataSourceHolder.markSlave();        } else {            // 標記為寫庫            DynamicDataSourceHolder.markMaster();        }    }    /**     * 判斷是否為讀庫     *      * @param methodName     * @return     */    private Boolean isSlave(String methodName) {        // 方法名以query、find、get開頭的方法名走從庫        return StringUtils.startsWithAny(methodName, getSlaveMethodStart());    }    /**     * 萬用字元匹配     *      * Return if the given method name matches the mapped name.     * <p>     * The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, as well as direct

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.