Java資料庫分表與多執行緒查詢結果彙總

Acelin_H發表於2021-07-27

今天接到一個需求:要對一個物理分表的邏輯表進行查詢統計。而資料庫用的是公司自己研發的產品,考慮的到公司產品的特點以及業務的需求,該邏輯表是按年月進行分表的,而非分割槽。我們來看一下,在按時間段進行查詢統計的時候,會有哪些問題:

  1. 需要對多個表查詢,且表個數不確定
  2. 時間跨度越大,查詢等等表個數越多,對應查詢時間也會越長

如何解決?一起來看一下


分表與分割槽


目的

既然談到資料的分表與分割槽,那我們來簡單瞭解一下。先說一下分表與分割槽的目的。我們日常開發中都會經常遇到百萬或千萬級的資料大表,這些表資料量大,資料增速快,不用太久就會造成在查詢或修改資料庫資料的時候造成效能低下的問題,聯合查詢的時候,情況可能更糟。一次有必要對原來的表進行改造設計。這時候資料庫分割槽和分表技術就應運而生了

區別
  • 分表

    分表是將一個大表按照一定的規則分解成多張子表,而各個子表儲存空間彼此獨立。

  • 分割槽

    分割槽也是按照一定的規則進行資料劃分,對各部分資料各自儲存,但在處理邏輯上,雜湊存放的資料還是屬於同一張大表。

    依賴於資料庫實現,對程式遮蔽,減輕程式設計師程式設計壓力


分表邏輯下的多執行緒查詢與資料彙總


回到文首提到的情況,當前的情況是分表,分表的劃分依據是根據年月劃分,一個月一張表。意味著當我們要統計跨多個隔離單位的資料進行統計時,要自己去實現的對分散在多個表中資料的查詢彙總處理。

通常表名會帶有劃分依據的資訊,比如按年月劃分,表名格式一般為TABLE_NAME_YYYYMM


確定資料表

當前的需求是對一段時間內的資料進行統計,時間單位精確到月份。一次當我們根據服務入參拿到開始月份和結束月份後,要先得到所有涉及的月份。我們可以計算出將所有月份並儲存在一個List中,方便我們查詢各個表時進行表名的拼接。程式碼實現如下

/**
 * 獲取時間段內所有月份集合
 * @param beginMonth 開始年月
 * @param endMonth 結束年月
 **/
private List<String> getMonths(String beginMonth,String endMonth){

    List<String> result = new ArrayList<>();

    Date beginDate = DateUtils.getDate("yyyyMM",beginMonth);
    Date endDate = DateUtils.getDate("yyyyMM",endMonth);

    if (beginDate.after(endDate)) {
        throw new BusiException("時間入參非法");
    }

    result.add(beginMonth);

    Calendar cal = Calendar.getInstance();
    Date originalDate = beginDate;

    while (endDate.after(originalDate)) {

        cal.setTime(originalDate);
        cal.add(Calendar.MONTH, 1);
        originalDate = cal.getTime();
        result.add(DateUtils.getFormatDate(originalDate).substring(0,6));
    }

    return result;
}

確認執行緒個數

拿到所有月份後,進行分多執行緒處理的操作,增加單位時間內查詢表的個數,以此縮短查詢時間,通常我們都利用執行緒池來進行多執行緒操作。這裡會涉及執行緒池大小的考慮問題,可以參考以下博文:計算執行緒池最佳執行緒數。我們姑且用CPU複雜型公式進行計算

int cpuNums = Runtime.getRuntime().availableProcessors() + 1;

均勻分配資料

確定好執行緒的大小,我們還要考慮一個問題,那就是我們如何為一個執行緒均勻地分配資料的處理量,在當前的需求下,就是如何均勻地為每個執行緒分配對應處理的月份,可以參考以下程式碼:

/**
 * 平衡分組演算法 - 已知分配份數
 * @param sourceList 資料來源
 * @param groupNum 被非配份數
 **/
public static <T> List<T>[] spiltDataList(List<T> sourceList,int groupNum){

    List<T> [] group = new List[groupNum];
    /* 初始化陣列 */
    for (int i = 0 ; i < groupNum ; i++) {
        group[i] = new ArrayList<>();
    }

    int sourceSize = sourceList.size();

    int batchNum = sourceSize % groupNum == 0 ? sourceSize / groupNum : sourceSize / groupNum + 1;

    for (int i = 1; i <= batchNum ; i++){

        if (i == batchNum){
            int finalBatchNum = sourceSize - (i - 1) * groupNum;
            for (int j = 0 ; j < finalBatchNum ; j++){
                group[j].add(sourceList.get((i - 1) * groupNum + j));
            }
        }else {
            for (int j = 0 ; j < groupNum ; j++){
                group[j].add(sourceList.get((i - 1) * groupNum + j));
            }
        }

    }
    return group;

}

多執行緒實現

要對所有子執行緒進行彙總,就必須使用Callable和Future的方式來實現多執行緒,我們就可以拿到每個子執行緒的查詢返回,進而彙總分析處理。關於多執行緒實現方式,可以參考Java多執行緒事務管理中對多執行緒實現方式的介紹

以下為核心程式碼實現

/**
 * 銀行扣費查詢 - 多執行緒方案
 * @param qryType 查詢型別
 * @param qryValue 查詢值
 * @param payType 扣費型別
 * @param beginMonth 開始年月
 * @param endMonth 結束年月
 **/
public List<CollInfoQueryBo> collInfoQueryByThread(
        String qryType,Long qryValue,String payType,String beginMonth,String endMonth)
throws InterruptedException,ExecutionException{

    List<CollInfoQueryBo> collInfoQueryBos = new ArrayList<>();

    List<String> months = getMonths(beginMonth,endMonth);
    int cpuNums = Runtime.getRuntime().availableProcessors() + 1;
    int totalNum = months.size();

    int threadNum;

    if (totalNum < cpuNums){
        threadNum = totalNum;
    }else {
        threadNum = cpuNums;
    }

    /* 分執行緒處理 */
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadNum);
    CountDownLatch endLock = new CountDownLatch(threadNum);
    BlockingQueue<Future<List<CollInfoQueryBo>>> queue = new LinkedBlockingQueue<>();

    List<String>[] stringList = spiltDataList(months,threadNum);

    for (List<String> monthList : stringList) {

        Future<List<CollInfoQueryBo>> future = fixedThreadPool.submit(new Callable<List<CollInfoQueryBo>>() {
            @Override
            public List<CollInfoQueryBo> call() throws Exception {

                List<CollInfoQueryBo> collInfoQueryBoList = getAllMonthResult(monthList,qryType,qryValue);

                endLock.countDown();

                return collInfoQueryBoList;
            }
        });

        queue.add(future);
    }

    endLock.await();

    /* 彙總結果 */
    for(Future<List<CollInfoQueryBo>> future : queue) {
        List<CollInfoQueryBo> currentThreadList = future.get();
        collInfoQueryBos.addAll(currentThreadList);
    }
    fixedThreadPool.shutdown(); //關閉執行緒池

    return collInfoQueryBos;

}

相關文章