Spring 整合MySql實(shí)現(xiàn)無侵入式的讀寫分離

一兜喻、背景描述

項(xiàng)目發(fā)展到一定的階段伏尼,數(shù)據(jù)庫可能成為訪問性能的一個(gè)瓶頸咆蒿,并且我們發(fā)現(xiàn)數(shù)據(jù)庫的讀寫基本上符合2-8定律,因此有必要對(duì)數(shù)據(jù)庫進(jìn)行主從備份和讀寫分離秩冈,通過擴(kuò)展讀數(shù)據(jù)庫的數(shù)目來橫線的分散讀數(shù)取數(shù)據(jù)的訪問壓力本缠。

在之前的文章中有介紹過Mysql層面的主從復(fù)制的原理以及環(huán)境的搭建,這里不再贅述入问,直接介紹如何對(duì)一個(gè)原來只有一個(gè)讀寫庫的項(xiàng)目進(jìn)行改造搓茬,使其讀寫分離犹赖。

為了最小化項(xiàng)目的改造成本队他,我們自然希望對(duì)于原有的項(xiàng)目的代碼改造量最小卷仑,因此我們考慮通過在dao層對(duì)Mybatis的Executor執(zhí)行語句進(jìn)行攔截:如果是事務(wù)性的操作或者INSERT、UPDATE麸折、DELETE等操作锡凝,則使用主庫(寫庫);如果是SELECT查詢語句垢啼,則使用從庫(讀庫)窜锯。

代碼實(shí)現(xiàn)

在dao package下新增一個(gè)split的子package,在該package下依次編寫如下的三個(gè)類:

2.1 代碼編寫

2.1.1 編寫DynamicDatasource

該類繼承自AbstractRoutingDataSource芭析,通過復(fù)寫determineCurrentLookupKey()方法決定使用主庫還是從庫锚扎。

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

public class DynamicDataSource extends AbstractRoutingDataSource {

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

2.1.2 編寫DynamicDataSourceHolder

該類使用一個(gè)ThreadLocal的變量用于保存實(shí)現(xiàn)線程安全的主從庫信息的存取。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DynamicDataSourceHolder {

    private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceHolder.class);

    public static final String DB_MASTER = "master";

    public static final String DB_SLAVE = "slave";

    private static ThreadLocal<String> contextHolder = new ThreadLocal<>();

    /**
     * 獲取線程的dbType
     *
     * @return
     */
    public static String getDbType() {
        String db = contextHolder.get();
        if (db == null) {
            db = DB_MASTER;
        }
        return db;
    }

    /**
     * 設(shè)置線程的dbType
     *
     * @param str
     */
    public static void setDbType(String str) {
        logger.debug("所使用的數(shù)據(jù)源為:" + str);
        contextHolder.set(str);
    }

    /**
     * 清理連接類型
     */
    public static void clearDbType() {
        contextHolder.remove();
    }

}

2.1.3 編寫DynamicDatasourceInterceptor

自定義的數(shù)據(jù)源攔截器DynamicDatasourceInterceptor 馁启,繼承自Mybatis的Interceptor攔截器驾孔,用于在Exectutor執(zhí)行時(shí)對(duì)實(shí)際執(zhí)行語句進(jìn)行攔截,確定使用主庫還是從庫惯疙,并且將信息保存到線程安全的DynamicDataSourceHolder中翠勉,后面在具體確定使用哪個(gè)數(shù)據(jù)源時(shí),會(huì)到DynamicDataSourceHolder中進(jìn)行查詢霉颠。

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.swing.tree.RowMapper;
import java.util.Locale;
import java.util.Properties;

/**
 * Mybatis級(jí)別的攔截器
 * <p>
 * 該攔截器會(huì)去攔截傳遞進(jìn)來的SQL信息:
 * - 如果是INSERT对碌、UPDATE則采用寫數(shù)據(jù)源(Master)
 * - 如果是SELECT則采用讀數(shù)據(jù)源(Slave)
 */
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class DynamicDatasourceInterceptor implements Interceptor {

    private static Logger logger = LoggerFactory.getLogger(DynamicDatasourceInterceptor.class);

    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";

    /**
     * SQL語句攔截
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String lookupKey = DynamicDataSourceHolder.DB_MASTER;
        Object[] objects = invocation.getArgs();
        MappedStatement ms = (MappedStatement) objects[0];
        // 是否啟用事務(wù)支持,檢測(cè)@Transactional注解是否啟用蒿偎,啟用這里就為true,否則為false
        boolean synchronizationActive = TransactionSynchronizationManager.isActualTransactionActive();
        if (synchronizationActive != true) {
            //讀方法
            if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                // selectKey為自增id查詢主鍵(SELECT_LAST_INSERT_ID())方法則使用主庫
                if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                    lookupKey = DynamicDataSourceHolder.DB_MASTER;
                } else {
                    BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                    String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
                    if (sql.matches(REGEX)) {
                        lookupKey = DynamicDataSourceHolder.DB_MASTER;
                    } else {
                        lookupKey = DynamicDataSourceHolder.DB_SLAVE;
                    }
                }
            }
        } else {
            lookupKey = DynamicDataSourceHolder.DB_MASTER;
        }
        logger.debug("設(shè)置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), lookupKey, ms.getSqlCommandType().name());
        DynamicDataSourceHolder.setDbType(lookupKey);
        return invocation.proceed();
    }

    /**
     * 決定返回本體還是編織好的代理類,
     * 代理類會(huì)去調(diào)用intercept方法決定是使用主庫還是從庫
     *
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {//Executor是用來支持一系列增刪改查操作的朽们,因此需要對(duì)該對(duì)象進(jìn)行攔截,其他的放過
            return Plugin.wrap(target, this);
        } else {//返回本體诉位,不做攔截
            return target;
        }
    }

    /**
     * 做一些相關(guān)的參數(shù)設(shè)置骑脱,不是關(guān)鍵
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

    }
}

2.2 配置變更

原來只有一個(gè)數(shù)據(jù)庫部署在192.168.91.134這臺(tái)機(jī)器上,后面為它在192.168.91.137上配置了一個(gè)從庫不从,訪問的端口惜姐、賬號(hào)、密碼都一樣椿息。

2.2.1 修改數(shù)據(jù)庫配置參數(shù)

原始的jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.91.134:3306/o2o?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=123

修改后的jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.master.url=jdbc:mysql://192.168.91.134:3306/o2o?useUnicode=true&characterEncoding=utf8
jdbc.master.username=root
jdbc.master.password=123

jdbc.slave.url=jdbc:mysql://192.168.91.137:3306/o2o?useUnicode=true&characterEncoding=utf8
jdbc.slave.username=root
jdbc.slave.password=123

2.2.3 修改數(shù)據(jù)源配置

原始的Spring-dao.xml中關(guān)于dataSource的配置:

<!--數(shù)據(jù)庫連接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--配置連接池屬性-->
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <!--c3p0連接池私有屬性-->
        <property name="maxPoolSize" value="30"/>
        <property name="minPoolSize" value="10"/>

        <!--關(guān)閉連接后不自動(dòng)commit-->
        <property name="autoCommitOnClose" value="false"/>
        <!--獲取連接超時(shí)時(shí)間設(shè)置-->
        <property name="checkoutTimeout" value="10000"/>
        <!--當(dāng)獲取連接失敗時(shí)重試的次數(shù)-->
        <property name="acquireRetryAttempts" value="2"/>
    </bean>

修改后的Spring-dao.xml中關(guān)于dataSource的配置:

<!--數(shù)據(jù)庫連接池-->
    <bean id="abstractDatasource" abstract="true" class="com.mchange.v2.c3p0.ComboPooledDataSource"
          destroy-method="close">
        <!--c3p0連接池私有屬性-->
        <property name="maxPoolSize" value="30"/>
        <property name="minPoolSize" value="10"/>

        <!--關(guān)閉連接后不自動(dòng)commit-->
        <property name="autoCommitOnClose" value="false"/>
        <!--獲取連接超時(shí)時(shí)間設(shè)置-->
        <property name="checkoutTimeout" value="10000"/>
        <!--當(dāng)獲取連接失敗時(shí)重試的次數(shù)-->
        <property name="acquireRetryAttempts" value="2"/>
    </bean>

    <!--配置主庫連接池屬性-->
    <bean id="master" parent="abstractDatasource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.master.url}"/>
        <property name="user" value="${jdbc.master.username}"/>
        <property name="password" value="${jdbc.master.password}"/>
    </bean>

    <!--配置從庫連接池屬性-->
    <bean id="slave" parent="abstractDatasource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.slave.url}"/>
        <property name="user" value="${jdbc.slave.username}"/>
        <property name="password" value="${jdbc.slave.password}"/>
    </bean>

    <!--
        配置動(dòng)態(tài)數(shù)據(jù)源:
        這兒的targetDatasources就是路由數(shù)據(jù)源所對(duì)應(yīng)的名稱歹袁。
    -->
    <bean id="dynamicDatasource" class="com.netease.o2o.dao.split.DynamicDataSource">
        <property name="targetDataSources">
            <map>
                <entry value-ref="master" key="master"></entry>
                <entry value-ref="slave" key="slave"></entry>
            </map>
        </property>
    </bean>

    <!--延遲懶加載機(jī)制:是在mybatis生成sql語句時(shí)才去執(zhí)行-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
        <property name="targetDataSource">
            <ref bean="dynamicDatasource"/>
        </property>
    </bean>

三、測(cè)試

3.1 測(cè)試查詢

 2018-12-17 16:14:04.859 [main] DEBUG c.n.o2o.dao.split.DynamicDatasourceInterceptor - 設(shè)置方法[com.netease.o2o.dao.AreaDao.findAll] use [master] Strategy, SqlCommandType [SELECT]..
2018-12-17 16:14:04.863 [main] DEBUG com.netease.o2o.dao.split.DynamicDataSourceHolder - 所使用的數(shù)據(jù)源為:slave
2018-12-17 16:14:04.871 [main] DEBUG com.netease.o2o.dao.AreaDao.findAll - ==>  Preparing: SELECT area_id,area_name,priority,create_time,last_edit_time FROM tb_area ORDER BY priority DESC
2018-12-17 16:14:04.894 [main] DEBUG com.netease.o2o.dao.AreaDao.findAll - ==> Parameters:
 2018-12-17 16:14:04.909 [main] DEBUG com.netease.o2o.dao.AreaDao.findAll - <==      Total: 2

3.2 測(cè)試插入

2018-12-17 16:13:09.348 [main] DEBUG c.n.o2o.dao.split.DynamicDatasourceInterceptor - 設(shè)置方法[com.netease.o2o.dao.ShopDao.insertShop] use [master] Strategy, SqlCommandType [INSERT]..
2018-12-17 16:13:09.351 [main] DEBUG com.netease.o2o.dao.split.DynamicDataSourceHolder - 所使用的數(shù)據(jù)源為:master
2018-12-17 16:13:09.357 [main] DEBUG com.netease.o2o.dao.ShopDao.insertShop - ==>  Preparing: INSERT INTO tb_shop (owner_id,area_id,shop_category_id,shop_name,shop_description,shop_address,phone,shop_image, priority,create_time,last_edit_time,enable_status,advice) VALUES (?,?,?,?,? ,?,?,?,?,?,?,?,?)
2018-12-17 16:13:09.387 [main] DEBUG com.netease.o2o.dao.ShopDao.insertShop - ==> Parameters: 1(Long), 1(Integer), 1(Long), test(String), test(String), test(String), test(String), (String), 1(Integer), 2018-12-17 16:13:09.336(Timestamp), 2018-12-17 16:13:09.336(Timestamp), 1(Integer), 審核中(String)
2018-12-17 16:13:09.388 [main] DEBUG com.netease.o2o.dao.ShopDao.insertShop - <==    Updates: 1
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寝优,一起剝皮案震驚了整個(gè)濱河市条舔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌乏矾,老刑警劉巖孟抗,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迁杨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡凄硼,警方通過查閱死者的電腦和手機(jī)铅协,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摊沉,“玉大人狐史,你說我怎么就攤上這事∷的” “怎么了骏全?”我有些...
    開封第一講書人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尼斧。 經(jīng)常有香客問我姜贡,道長,這世上最難降的妖魔是什么棺棵? 我笑而不...
    開封第一講書人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任楼咳,我火速辦了婚禮,結(jié)果婚禮上律秃,老公的妹妹穿的比我還像新娘爬橡。我一直安慰自己,他們只是感情好棒动,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開白布糙申。 她就那樣靜靜地躺著,像睡著了一般船惨。 火紅的嫁衣襯著肌膚如雪柜裸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評(píng)論 1 305
  • 那天粱锐,我揣著相機(jī)與錄音疙挺,去河邊找鬼。 笑死怜浅,一個(gè)胖子當(dāng)著我的面吹牛铐然,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播恶座,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼搀暑,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了跨琳?” 一聲冷哼從身側(cè)響起自点,我...
    開封第一講書人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎脉让,沒想到半個(gè)月后桂敛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體功炮,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年术唬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了薪伏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡碴开,死狀恐怖毅该,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情潦牛,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布挡育,位于F島的核電站巴碗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏即寒。R本人自食惡果不足惜橡淆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望母赵。 院中可真熱鬧逸爵,春花似錦、人聲如沸凹嘲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽周蹭。三九已至趋艘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凶朗,已是汗流浹背瓷胧。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留棚愤,地道東北人搓萧。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像宛畦,于是被迫代替她去往敵國和親瘸洛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 31,938評(píng)論 2 89
  • 電影《后會(huì)無期》中有一句臺(tái)詞“喜歡就會(huì)放肆刃永,但愛就是克制货矮。” 在我淺略地觀覽了一回王爾德的一生中似乎得到了共鳴斯够。 ...
    方目閱讀 6,792評(píng)論 1 3
  • 雨水切斷光的視線 站在黑暗邊緣 厚厚壁壘 腳下粘稠 有光在褶皺 僵化的脊椎 粘連的眼皮 也許太多的夜晚 沒有光洗禮...
    達(dá)悟希閱讀 409評(píng)論 0 6