架構設計:系統存儲(11)——MySQL主從方案業務連接透明化(上)

轉自http://blog.csdn.net/yinwenjie/article/details/52980883

1、MySQL主從方案業務層的問題

在之前的文章中,我們提到MySQL一主多從集羣模式下,對上層業務系統的訪問帶來了一些問題。本編文章中我們將深入分析這個問題,並介紹如何對這個問題進行改進。MySQL一主多從集羣對上層業務系統帶來的主要問題是,上層業務系統需要自行控制本次MySQL數據操作需要訪問MySQL集羣中的哪個節點。產生這個問題的主要原因,是因爲MySQL一主多從集羣本身並沒有提供現成功能,將集羣中的節點打包成統一服務並向外提供。

這裏寫圖片描述

在上圖所示的MySQL集羣中,有一個Master節點負責所有的寫事務操作,還有兩個Salve節點分別負責訂單模塊的讀操作和用戶模塊的讀操作,而這個架構方案中由於沒有中間管理層,所以到底訪問哪一個MySQL服務節點的判斷工作全部需要由上層業務系統自行判斷。那麼解決這個問題的思路也就比較清晰了:我們需要通過一些手段自行爲業務層的訪問增加一箇中間層,以減少業務開發人員的維護工作。
2、改進方式一:使用Spring套件屏蔽細節

如果您的工程使用了spring組件,那這個問題可以使用Spring配置問題進行改善。但這個方式只能算是改善問題,不能算作完全解決了問題。這是因爲雖然通過Spring配置後,業務開發人員不需要再爲“訪問哪個數據庫節點”操碎了心,但是Spring的配置文件依然是存在於業務系統中,當下層MySQL集羣節點發生變化時,業務系統就需要改變配置信息並且重新部署;當MySQL集羣現有節點發生故障時,上層業務系統也需要變更配置信息並重新部署。這種配置的方法並不能實現數據訪問邏輯的完全脫耦。

下面我們給出一個示例,在這個示例中我們使用spring 3.X 版本 + hibernate 4.X 版本 + c3p0 + MySQL JDBC 實現在業務系統中訪問數據庫節點的規則配置。

這裏寫圖片描述

如上圖所示,我們在業務系統中建立了兩個數據源:writeSessionFactory、readSessionFactory,分別負責業務數據的寫操作和讀操作。當下面的MySQL集羣增加新的讀節點或者集羣中現有節點發生變化時,spring的配置文件也要做相應的配置變化:

寫操作涉及的數據源和AOP點配置
<!-- 工程中和讀寫數據源分離無關的Spring配置信息,在這裏就不進行贅述了 -->
......
<!-- 這個數據源連接到maseter節點, 作爲寫操作的數據源-->
<bean id="writedataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <property name="driverClass"><value>${writejdbc.driver}</value></property>
    <property name="jdbcUrl"><value>${writejdbc.url}</value></property>
    <property name="user"><value>${writejdbc.username}</value></property>
    <property name="password"><value>${writejdbc.password}</value></property>
    <property name="minPoolSize"><value>${writec3p0.minPoolSize}</value></property>
    <property name="maxPoolSize"><value>${writec3p0.maxPoolSize}</value></property>
    <property name="initialPoolSize"><value>${writec3p0.initialPoolSize}</value></property>
    <property name="maxIdleTime"><value>${writec3p0.maxIdleTime}</value></property>
    <property name="acquireIncrement"><value>${writec3p0.acquireIncrement}</value></property>
</bean>

<!-- 數據層會話工廠和數據源的映射關係 -->
<bean id="writeSessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="writedataSource" />
    <property name="namingStrategy">
        <bean class="org.hibernate.cfg.ImprovedNamingStrategy" />
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">${writehibernate.dialect}</prop>
            <prop key="hibernate.show_sql">${writehibernate.show_sql}</prop>
            <prop key="hibernate.format_sql">${writehibernate.format_sql}</prop>
            <prop key="hibernate.hbm2ddl.auto">${writehibernate.hbm2ddl.auto}</prop>
            <prop key="hibernate.current_session_context_class">org.springframework.orm.hibernate4.SpringSessionContext</prop>
        </props>
    </property>
</bean>
<bean id="writetransactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="writeSessionFactory" />
</bean>

<!-- 
AOP設置,在templateSSHProject.dao.writeop包或者子包中
涉及以下名字的方法,都要開啓事務託管。
並且在拋出任何異常的情況下,spring都要回滾事務
-->
<aop:config> 
    <aop:pointcut id="writedao" expression="execution(* templateSSHProject.dao.writeop..*.* (..))" />
    <aop:advisor advice-ref="writetxAdvice" pointcut-ref="writedao" />
</aop:config>
<tx:advice id="writetxAdvice" transaction-manager="writetransactionManager">
    <tx:attributes>
        <tx:method name="save*" rollback-for="java.lang.Exception" propagation="REQUIRED" />
        <tx:method name="update*" rollback-for="java.lang.Exception" propagation="REQUIRED" />
        <tx:method name="delete*" rollback-for="java.lang.Exception" propagation="REQUIRED" />
        <tx:method name="modify*" rollback-for="java.lang.Exception" propagation="REQUIRED" />
        <tx:method name="create*" rollback-for="java.lang.Exception" propagation="REQUIRED" />
        <tx:method name="remove*" rollback-for="java.lang.Exception" propagation="REQUIRED" />
        <tx:method name="*" read-only="true" />
    </tx:attributes>
</tx:advice>
......
讀操作涉及的數據源和AOP點配置,和寫操作數據源配置類似,各位讀者只需要注意不同點
......
<!-- 這個數據源連接到salve節點, 作爲讀操作的數據源 -->
<bean id="readdataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <property name="driverClass"><value>${readjdbc.driver}</value></property>
    <property name="jdbcUrl"><value>${readjdbc.url}</value></property>
    <property name="user"><value>${readjdbc.username}</value></property>
    <property name="password"><value>${readjdbc.password}</value></property>
    <property name="minPoolSize"><value>${readc3p0.minPoolSize}</value></property>
    <property name="maxPoolSize"><value>${readc3p0.maxPoolSize}</value></property>
    <property name="initialPoolSize"><value>${readc3p0.initialPoolSize}</value></property>
    <property name="maxIdleTime"><value>${readc3p0.maxIdleTime}</value></property>
    <property name="acquireIncrement"><value>${readc3p0.acquireIncrement}</value></property>
</bean>

<!-- 數據層會話工廠和數據源的映射關係,基本上和write的設置一致 -->
<bean id="readSessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="readdataSource" />
    <property name="namingStrategy">
        <bean class="org.hibernate.cfg.ImprovedNamingStrategy" />
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">${readhibernate.dialect}</prop>
            <prop key="hibernate.show_sql">${readhibernate.show_sql}</prop>
            <prop key="hibernate.format_sql">${readhibernate.format_sql}</prop>
            <prop key="hibernate.hbm2ddl.auto">${readhibernate.hbm2ddl.auto}</prop>
            <prop key="hibernate.current_session_context_class">org.springframework.orm.hibernate4.SpringSessionContext</prop>
        </props>
    </property>
</bean>
<bean id="readtransactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="readSessionFactory" />
</bean>

<!-- 
AOP設置,和寫操作事務不同的點也在這裏
這裏不需要設置任何方法方法開啓事務託管
甚至不需要配置這段AOP切面
-->
<aop:config> 
    <aop:pointcut id="readdao" expression="execution(* templateSSHProject.dao.readop..*.* (..))" />
    <aop:advisor advice-ref="readtxAdvice" pointcut-ref="readdao" />
</aop:config>
<tx:advice id="readtxAdvice" transaction-manager="readtransactionManager">
    <tx:attributes>
        <tx:method name="*" read-only="true" />
    </tx:attributes>
</tx:advice>
......

要看懂以上的Spring配置信息,首先請確定您接觸過Spring組件。以上配置主要使用XML文件定義的方式指定需要使用“寫數據源”的方法名,使用AOP切面爲這些指定的方法名開啓事務託管。在實際使用過程中,各位讀者也可以使用Java代碼註解的方式在指定的包範圍內標記需要開啓事務託管的方法。只需要在配置文件中增加一段說明信息“”

這樣的讀寫數據源分離的方式,只會影響業務開發人員在數據層的操作,對顯示層和業務邏輯層沒有任何影響。這裏再給出工程中數據層部分的包結構,這個結構和AOP配置中的切面掃描點“expression”有關。從以下給出的結構圖可以看出,在數據層進行的數據讀操作和數據寫操作是分離的,這樣可以避免業務開發人員在編寫代碼時發生混淆(例如在負責讀操作的工程包中進行寫操作)

這裏寫圖片描述

當然針對各位讀者自身的業務形態,您也可以將兩個數據源合併在一起混合使用,不過這可能會加重業務開發人員的維護工作。將讀寫操作數據源合併的方式也很簡單,基本上不需要更改任何配置,只需要將兩個sessionFactory注入到同一個AbstractRelationalDBDAO——爲DAO層設置的公共父類。

......
public abstract class AbstractReadRelationalDBDAO ...... {

    // AbstractRelationalDBDAO類的其它部分都省略了
    ......
    @Autowired
    @Qualifier("readSessionFactory")
    private SessionFactory readSessionFactory;

    @Autowired
    @Qualifier("writeSessionFactory")
    private SessionFactory sessionFactory;
    ......
}
......

本示例實現的Spring配置中只有一個負責寫操作數據源和一個負責讀操作數據源。那麼從理論上講這種Spring配置方式只能適應一主一從的MySQL集羣。當MySQL集羣中的節點發生,例如增加了一個Salve從節點,業務工程就會增加一個從節點的數據源配置信息,並且在工程的數據層(DAO層)增加新的代碼包。這顯然是很有問題的,甚至可以說整個業務工程基本無法維護,這是因爲再穩定的MySQL集羣也只能保證5個9的系統可用性(即99.999%),另外現在主流的集羣思想中本來就是假設集羣中的節點隨時可能出現問題,而業務系統顯然不可能在無法預知的情況下隨時改變配置信息並重新部署。

3、改進方式二:透明化中間層

那麼有沒有什麼辦法能夠解決以上的問題呢?既突破以上Spring配置方式只適應一主一從MySQL集羣的瓶頸,又不增加業務開發人員的維護難度,還能適應下層數據集羣隨時發生的節點故障。當然是有辦法的,使用我們已經在負載均衡專題介紹過的LVS,我們可以爲MySQL集羣中的多個讀節點構造一個透明層,使得它們可以作爲一個整體,並使用一個統一的訪問地址向上層業務系統提供數據讀取服務。

這裏寫圖片描述

如果您還不清楚LVS配置方式,可以參見我另外一專題中,專門介紹LVS的幾篇文章:《架構設計:負載均衡層設計方案(4)——LVS原理》、《架構設計:負載均衡層設計方案(5)——LVS單節點安裝》。需要注意的是,這裏選擇的負載方案應該工作在網絡協議的下層,例如OSI七層模型的鏈路層或者傳輸層。這是因爲上層系統連接MySQL服務節點主要基於TCP/IP協議而不是基於HTTP協議,例如MySQL的多數客戶端軟件(MySQL-Front、Navicat等)都使用MySQL原生連接協議,這個協議就是基於TCP/IP協議的,再例如絕大多數Java應用程序連接和調用MySQL操作所基於的JDBC API,也是基於TCP/IP協議。所以這裏使用的負載均衡方案不能使用Nginx這樣只支持Http協議的組件,而LVS組件可以很好的適應技術需求。

以上的改進方案中,我們只對MySQL集羣中的讀操作節點進行了改進,但是整個集羣還是沒有足夠的穩定保證。這是因爲MySQL集羣中寫操作節點目前還只有單個節點承載工作,新加入的LVS負載節點也只有單個節點承載工作。如果在生產環境下,以上這些節點出現故障無法工作將導致整個MySQL集羣崩潰。進一步的改進方式,就是爲集羣中的寫操作節點和LVS負載節點增加熱備方案,如下圖所示:

這裏寫圖片描述

上圖中我們使用Heartbeat + DRBD第三方組件的方式,爲MySQL Master節點複製了一個可以即時切換的處於“準備”狀態的備用節點。Heartbeat組件的作用和之前我們介紹過的keepalived組件類似,它用於監控兩個(或多個)節點的工作狀態,並在滿足宕機的判斷條件時完成浮動IP的切換和備用服務的啓動工作。DRBD組件是一個工作在Linunx系統下的,可以完成實時文件差異化同步的磁盤塊映射軟件,類似的軟件還有RSync。

有了Heartbeat + DRBD第三方組件的支持,就可以保證當MySQL集羣中的寫操作節點不能提供服務時,另一個等待工作的備份寫操作節點能夠及時的接過工作任務,並且這個備份節點上的數據庫表數據和之前崩潰的寫操作節點上的數據庫表數據是一致的。這個方案還可以更換成Keepalived + RSync的第三方組件方案。

LVS節點的高可用方案,在之前的文章中已經介紹過了。不清楚的讀者可以參考我另一篇文章《架構設計:負載均衡層設計方案(7)——LVS + Keepalived + Nginx安裝及配置》,只不過文章中的需要被保證高可用性的組件由Nginx替換成了MySQL服務。不過這樣的讀節點組織方式,也有一些問題:雖然這些讀節點通過負載均衡的方式可以分擔各自的工作壓力,但是這些讀操作節點不能按照上層業務的不同,分模塊提供獨立的、有個性的查詢操作服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章