數據庫讀寫分離方法淺析

原創作品,出自 “曉風殘月xj” 博客,歡迎轉載,轉載時請務必註明出處(http://blog.csdn.net/xiaofengcanyuexj)。

由於各種原因,可能存在諸多不足,歡迎斧正!


       隨着業務的快速發展,表不斷增多,結構不斷擴大,數據量也在慢慢積累,最近數據庫DB壓力較大,有些慢查詢日誌 。個人總結原因如下:表數量和結構日漸複雜,單表數量增多,聯表查詢等大sql ,應用層大事務等等。

1、表數量和結構日漸複雜
     隨着業務的擴張,這種規模帶來的問題最通用的解決方案應該是分庫分表。分庫有2種:水平拆分和垂直拆分。其中,垂直分庫是將不同業務含義的表拆分到不同的數據庫中;水平分庫是將相同表中不同業務含義的數據行拆分到不同的數據庫中。同理,分表也有垂直拆分和水平拆分的區別。其中,垂直分表是將表的不同字段拆分成不同的表;水平分表是將表的不同數據行拆分到不同表中,比如歷史數據切分,冷熱數據分離等等。

2、單表數量增多

      可以通過分表分庫解決,不過相對工作量大點。簡單直接粗暴的解決方案是將過期或者無效或者相對不重要的數據分離或者清理掉,可以定期同步到相對隔絕的歷史表中,也可以應用層定時任務直接刪除數據(物理刪除和邏輯刪除都可以),不過最好是定時任務或者手工同步到歷史表中備份,以防以後用到。

3、聯表查詢等大sql 

    拆分聯表查詢等大sql語句,可以減少大sql造成的讀寫鎖競爭、利用DB本身的查詢緩存、方便應用層預加載設置緩存,但拆分sql分多次查詢潛在的問題是佔用網絡資源較多,可能應用機器會佔用很大內存保存臨時查詢結果等。當然,一般內網網速和機器內存都不會是性能瓶頸,所以可以拆分大sql是不錯的選擇。將直接mapper訪問抽象成dao層,對service透明。

4、應用層大事務

    事務的特性是老生常談的ACID,即原子性,一致性,隔離性,持久性。一般web框架對於事務的支持應該都是比較友好的,比如spring mvc只需要配置+註解@Transactional。事務容易加劇鎖競爭,即便有些數據庫,比如mysql已經採用MVCC(高版本併發控制),但是在某些關鍵共享變量上也會加鎖,會比較影響性能。


   上面都是快速發展的數據庫DB容易遇到的問題和潛在的解決方案,當然比較膚淺,考慮到接觸到的系統出現慢查詢時從庫slave並未完全使用,更多的是容災備份,所以假期嘗試將部分讀請求打到slave上,做到讀寫分離,master寫數據+少量讀請求,slave只讀數據。一般數據庫主從同步都不可避免存在一定的時延,考慮到有些主流程對時效性要求比較強的讀請求,應該讀主庫,這類問題的解決方案也有很多,主要有如下幾種:

1、半同步複製,即等待寫請求主庫同步待從庫後,寫請求才返回,這樣讀從庫就能讀到最新的數據,mysql。

2、強制讀主庫,對於特定的讀請求,直接強制讀主庫。

3、數據庫中間件,所有讀寫請求都走中間件,寫請求到主庫,記錄所有路由到寫庫的key,在經驗主從同步時間窗口內,有讀請求訪問中間件,就把這個key上的讀請求路由到主庫。

4、緩存寫key法,數據庫中間件方案較重,較輕的是應用層使用緩存,當寫請求發生的時候同時向緩存中插入一條帶有過期失效時間的記錄,當讀請求到達時先讀緩存key,如果沒有值,則直接讀從庫;否則強制讀主庫。         

。。。

     各種方法 的優缺點及詳細介紹,可以參看DB主從一致性架構優化4種方法

    

     筆者實際的問題是嘗試讀寫分離,下面記錄一下讀寫分離方法,歡迎斧正。DB讀寫分離理論上方案比較多,如中間件轉發、應用層分離,數據庫驅動等等,各種方法的優缺點如下:

1、中間件轉發

通過mysql中間件做主從集羣,Mysql Proxy、Amoeba、Atlas等中間件貌似都能符合需求。

優點:對應用透明

缺點:需要代理,增加網絡等性能開銷

2、應用層分離

應用層路由數據源實現讀寫分離,通過AOP或者註解來動態選擇數據源

優點:無需中間件,策略可選,可用來負載均衡

缺點:耦合度高

3、數據庫驅動

Replication Driver或者使用Replication協議頭,Replication Driver根據connection的readonly屬性路由數據源,數據庫驅動

優點:對應用透明,無需中間件

缺點:需要DB支持Replication協議


Java web可以分爲兩個層次:JDBC層的封裝,ORM框架層的實現。

  結合實際情況較輕的方案是應用層分離,比如配置不同mapper訪問主從數據源、AOP切片路由讀寫請求等。

1、不同mapper訪問主從數據源一般來說,對於不同的數據源使用不同包的mapper訪問,就可以讀寫分離,但是幾乎相同代碼要拷貝多份

2、AOP切片路由讀寫請求利用反射,動態決定運行時的數據源,實現讀寫分離

   編寫動態數據源路由 DynamicDataSource類繼承AbstractRoutingDataSource,並實現determineCurrentLookupKey()方法,determineCurrentLookupKey()是spring數據庫連接池在建立連接時都要調用的。然後通過在mapper或者dao層切片,感覺最好在dao層切片,所有事務都定義在dao層;這樣對於事務的處理也比較方便,因爲要保持相同事務內的不同連接到同一個數據源(當然是master了),可以單獨對事務註解@transactional進行切片。

import com.google.common.collect.ImmutableMap;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;

 /**
 * 實現描述:切換動態數據源
 *
 * @author jin.xu
 * @version v1.0.0
 * @see
 * @since 16-10-6 下午3:40
 */
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> DAAL_HOLDER = new ThreadLocal<String>();
    @Resource
    private DataSource masterDataSource;
    @Resource
    private DataSource slaveDataSource;

    @Override
    public void afterPropertiesSet() {
        setTargetDataSources(ImmutableMap.<Object, Object> of("master", masterDataSource, "slave", slaveDataSource));
        setDefaultTargetDataSource(spiderDataSource);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DAAL_HOLDER.get();
    }

    public static void switchToMaster() {
        DAAL_HOLDER.set("master");
    }

    public static void switchToSlave() {
        DAAL_HOLDER.set("slave");
    }

    public static void reset() {
        DAAL_HOLDER.remove();
    }

}


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 實現描述:DB讀寫分離切片
 *
 * @author jin.xu
 * @version v1.0.0
 * @see
 * @since 16-10-6 下午4:53
 */
@Aspect
@Component
public class DBAspect {

    /**
     * 內部api監控
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution (* com.csdn.jinxu.dal.dao..*.*(..))")
    public Object dbLog(ProceedingJoinPoint joinPoint) throws Throwable {
        Long startTime = 0l;
        Long endTime = 0l;
        Object result = null;
        String method =null;
        try {
            startTime = System.currentTimeMillis();
            method = joinPoint.getSignature().getName();
            if(isSwitchToSlave(method)){
                DynamicDataSource.switchToSlave();
            }else{
                DynamicDataSource.switchToMaster();
            }
            result = joinPoint.proceed();
            endTime=System.currentTimeMillis();
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                String request = Arrays.toString(joinPoint.getArgs());
                String response =result.toString();
            } catch (Exception e) {
            }
            DynamicDataSource.reset();
        }
        return result;
    }

    /**
    *擴展配置粗略
    */
    private boolean isSwitchToSlave(String method){
        boolean isBFlag=false;
        if(null!=method&&(method.startsWith("find")
                ||method.startsWith("count")
                ||method.startsWith("get"))){
            isBFlag=true;
        }
        return isBFlag;
    }

}



    路漫漫其修遠兮,很多時候感覺想法比較幼稚。首先東西比較簡單,其次工作也比較忙,還好週末可以抽時間處理這個。由於相關知識積累有限,歡迎大家提意見斧正,在此表示感謝!後續版本會持續更新…





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