資料庫中介軟體 Sharding-JDBC 原始碼分析 —— 結果歸併

芋道原始碼_以德服人_不服就幹發表於2019-02-22

摘要: 原創出處 www.iocoder.cn/Sharding-JD… 「芋道原始碼」歡迎轉載,保留摘要,謝謝!

本文主要基於 Sharding-JDBC 1.5.0 正式版


???關注微信公眾號:【芋道原始碼】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
  3. 您對於原始碼的疑問每條留言將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢
  4. 新的原始碼解析文章實時收到通知。每週更新一篇左右
  5. 認真的原始碼交流微信群。

1. 概述

本文分享查詢結果歸併的原始碼實現。

正如前文《SQL 執行》提到的“分表分庫,需要執行的 SQL 數量從單條變成了多條”,多個SQL執行結果必然需要進行合併,例如:

SELECT * FROM t_order ORDER BY create_time複製程式碼

在各分片排序完後,Sharding-JDBC 獲取到結果後,仍然需要再進一步排序。目前有 分頁分組排序聚合列迭代 五種場景需要做進一步處理。當然,如果單分片SQL執行結果是無需合併的。在《SQL 執行》不知不覺已經分享了插入、更新、刪除操作的結果合併,所以下面我們一起看看查詢結果歸併的實現。


Sharding-JDBC 正在收集使用公司名單:傳送門
? 你的登記,會讓更多人蔘與和使用 Sharding-JDBC。傳送門
Sharding-JDBC 也會因此,能夠覆蓋更多的業務場景。傳送門
登記吧,騷年!傳送門

2. MergeEngine

MergeEngine,分片結果集歸併引擎。

// MergeEngine.java
/**
* 資料庫型別
*/
private final DatabaseType databaseType;
/**
* 結果集集合
*/
private final List<ResultSet> resultSets;
/**
* Select SQL語句物件
*/
private final SelectStatement selectStatement;
/**
* 查詢列名與位置對映
*/
private final Map<String, Integer> columnLabelIndexMap;

public MergeEngine(final DatabaseType databaseType, final List<ResultSet> resultSets, final SelectStatement selectStatement) throws SQLException {
   this.databaseType = databaseType;
   this.resultSets = resultSets;
   this.selectStatement = selectStatement;
   // 獲得 查詢列名與位置對映
   columnLabelIndexMap = getColumnLabelIndexMap(resultSets.get(0));
}

/**
* 獲得 查詢列名與位置對映
*
* @param resultSet 結果集
* @return 查詢列名與位置對映
* @throws SQLException 當結果集已經關閉
*/
private Map<String, Integer> getColumnLabelIndexMap(final ResultSet resultSet) throws SQLException {
   ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); // 後設資料(包含查詢列資訊)
   Map<String, Integer> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
   for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) {
       result.put(SQLUtil.getExactlyValue(resultSetMetaData.getColumnLabel(i)), i);
   }
   return result;
}複製程式碼
  • 當 MergeEngine 被建立時,會傳入 resultSets 結果集集合,並根據其獲得 columnLabelIndexMap 查詢列名與位置對映。通過 columnLabelIndexMap,可以很方便的使用查詢列名獲得在返回結果記錄列( header )的第幾列。

MergeEngine 的 #merge() 方法作為入口提供查詢結果歸併功能。

/**
* 合併結果集.
*
* @return 歸併完畢後的結果集
* @throws SQLException SQL異常
*/
public ResultSetMerger merge() throws SQLException {
   selectStatement.setIndexForItems(columnLabelIndexMap);
   return decorate(build());
}複製程式碼
  • #merge() 主體邏輯就兩行程式碼,設定查詢列位置資訊,並返回合適的歸併結果集介面( ResultSetMerger ) 實現。

2.1 SelectStatement#setIndexForItems()

// SelectStatement.java
/**
* 為選擇項設定索引.
* 
* @param columnLabelIndexMap 列標籤索引字典
*/
public void setIndexForItems(final Map<String, Integer> columnLabelIndexMap) {
   setIndexForAggregationItem(columnLabelIndexMap);
   setIndexForOrderItem(columnLabelIndexMap, orderByItems);
   setIndexForOrderItem(columnLabelIndexMap, groupByItems);
}複製程式碼
  • 部分查詢列是經過推到出來,在 SQL解析 過程中,未獲得到查詢列位置,需要通過該方法進行初始化。對這塊不瞭解的同學,回頭可以看下《SQL 解析(三)之查詢SQL》。? 現在不用回頭,皇冠會掉。
  • #setIndexForAggregationItem() 處理 AVG聚合計算列 推匯出其對應的 SUM/COUNT 聚合計算列的位置:

      private void setIndexForAggregationItem(final Map<String, Integer> columnLabelIndexMap) {
         for (AggregationSelectItem each : getAggregationSelectItems()) {
             Preconditions.checkState(columnLabelIndexMap.containsKey(each.getColumnLabel()), String.format("Can`t find index: %s, please add alias for aggregate selections", each));
             each.setIndex(columnLabelIndexMap.get(each.getColumnLabel()));
             for (AggregationSelectItem derived : each.getDerivedAggregationSelectItems()) {
                 Preconditions.checkState(columnLabelIndexMap.containsKey(derived.getColumnLabel()), String.format("Can`t find index: %s", derived));
                 derived.setIndex(columnLabelIndexMap.get(derived.getColumnLabel()));
             }
         }
      }複製程式碼
  • #setIndexForOrderItem() 處理 ORDER BY / GROUP BY 列不在查詢列 推匯出的查詢列的位置:

      private void setIndexForOrderItem(final Map<String, Integer> columnLabelIndexMap, final List<OrderItem> orderItems) {
          for (OrderItem each : orderItems) {
            if (-1 != each.getIndex()) {
                continue;
            }
            Preconditions.checkState(columnLabelIndexMap.containsKey(each.getColumnLabel()), String.format("Can`t find index: %s", each));
            if (columnLabelIndexMap.containsKey(each.getColumnLabel())) {
                each.setIndex(columnLabelIndexMap.get(each.getColumnLabel()));
            }
          }
      }複製程式碼

2.2 ResultSetMerger

ResultSetMerger,歸併結果集介面。

我們先來看看整體的類結構關係:

功能 上分成四種:

  • 分組:GroupByMemoryResultSetMerger、GroupByStreamResultSetMerger;包含聚合列
  • 排序:OrderByStreamResultSetMerger
  • 迭代:IteratorStreamResultSetMerger
  • 分頁:LimitDecoratorResultSetMerger

實現方式 上分成三種:

  • Stream 流式:AbstractStreamResultSetMerger
  • Memory 記憶體:AbstractMemoryResultSetMerger
  • Decorator 裝飾者:AbstractDecoratorResultSetMerger

什麼時候該用什麼實現方式?

  • Stream 流式:將資料遊標與結果集的遊標保持一致,順序的從結果集中一條條的獲取正確的資料。看完下文第三節 OrderByStreamResultSetMerger 可以形象的理解。
  • Memory 記憶體:需要將結果集的所有資料都遍歷並儲存在記憶體中,再通過記憶體歸併後,將記憶體中的資料偽裝成結果集返回。看完下文第五節 GroupByMemoryResultSetMerger 可以形象的理解。
  • Decorator 裝飾者:可以和前二者任意組合
// MergeEngine.java
/**
* 合併結果集.
*
* @return 歸併完畢後的結果集
* @throws SQLException SQL異常
*/
public ResultSetMerger merge() throws SQLException {
   selectStatement.setIndexForItems(columnLabelIndexMap);
   return decorate(build());
}

private ResultSetMerger build() throws SQLException {
   if (!selectStatement.getGroupByItems().isEmpty() || !selectStatement.getAggregationSelectItems().isEmpty()) { // 分組 或 聚合列
       if (selectStatement.isSameGroupByAndOrderByItems()) {
           return new GroupByStreamResultSetMerger(columnLabelIndexMap, resultSets, selectStatement, getNullOrderType());
       } else {
           return new GroupByMemoryResultSetMerger(columnLabelIndexMap, resultSets, selectStatement, getNullOrderType());
       }
   }
   if (!selectStatement.getOrderByItems().isEmpty()) {
       return new OrderByStreamResultSetMerger(resultSets, selectStatement.getOrderByItems(), getNullOrderType());
   }
   return new IteratorStreamResultSetMerger(resultSets);
}

private ResultSetMerger decorate(final ResultSetMerger resultSetMerger) throws SQLException {
   ResultSetMerger result = resultSetMerger;
   if (null != selectStatement.getLimit()) {
       result = new LimitDecoratorResultSetMerger(result, selectStatement.getLimit());
   }
   return result;
}複製程式碼

2.2.1 AbstractStreamResultSetMerger

AbstractStreamResultSetMerger,流式歸併結果集抽象類,提供從當前結果集獲得行資料。

public abstract class AbstractStreamResultSetMerger implements ResultSetMerger {

    /**
     * 當前結果集
     */
    private ResultSet currentResultSet;

    protected ResultSet getCurrentResultSet() throws SQLException {
        if (null == currentResultSet) {
            throw new SQLException("Current ResultSet is null, ResultSet perhaps end of next.");
        }
        return currentResultSet;
    }

    @Override
    public Object getValue(final int columnIndex, final Class<?> type) throws SQLException {
        if (Object.class == type) {
            return getCurrentResultSet().getObject(columnIndex);
        }
        if (int.class == type) {
            return getCurrentResultSet().getInt(columnIndex);
        }
        if (String.class == type) {
            return getCurrentResultSet().getString(columnIndex);
        }
        // .... 省略其他資料型別讀取類似程式碼
        return getCurrentResultSet().getObject(columnIndex);
    }
}複製程式碼

2.2.2 AbstractMemoryResultSetMerger

AbstractMemoryResultSetMerger,記憶體歸併結果集抽象類,提供從記憶體資料行物件( MemoryResultSetRow ) 獲得行資料。

public abstract class AbstractMemoryResultSetMerger implements ResultSetMerger {

    private final Map<String, Integer> labelAndIndexMap;
    /**
     * 記憶體資料行物件
     */
    @Setter
    private MemoryResultSetRow currentResultSetRow;

    @Override
    public Object getValue(final int columnIndex, final Class<?> type) throws SQLException {
        if (Blob.class == type || Clob.class == type || Reader.class == type || InputStream.class == type || SQLXML.class == type) {
            throw new SQLFeatureNotSupportedException();
        }
        return currentResultSetRow.getCell(columnIndex);
    }
}複製程式碼
  • 和 AbstractStreamResultSetMerger 對比,貌似區別不大?!確實,從抽象父類上看,兩種實現方式差不多。抽象父類提供給實現子類的是資料讀取的功能,真正的流式歸併、記憶體歸併是在子類實現上體現。
public class MemoryResultSetRow {

    /**
     * 行資料
     */
    private final Object[] data;

    public MemoryResultSetRow(final ResultSet resultSet) throws SQLException {
        data = load(resultSet);
    }

    /**
     * 載入 ResultSet 當前行資料到記憶體
     * @param resultSet 結果集
     * @return 行資料
     * @throws SQLException 當結果集關閉
     */
    private Object[] load(final ResultSet resultSet) throws SQLException {
        int columnCount = resultSet.getMetaData().getColumnCount();
        Object[] result = new Object[columnCount];
        for (int i = 0; i < columnCount; i++) {
            result[i] = resultSet.getObject(i + 1);
        }
        return result;
    }

    /**
     * 獲取資料.
     * 
     * @param columnIndex 列索引
     * @return 資料
     */
    public Object getCell(final int columnIndex) {
        Preconditions.checkArgument(columnIndex > 0 && columnIndex < data.length + 1);
        return data[columnIndex - 1];
    }

    /**
     * 設定資料.
     *
     * @param columnIndex 列索引
     * @param value 值
     */
    public void setCell(final int columnIndex, final Object value) {
        Preconditions.checkArgument(columnIndex > 0 && columnIndex < data.length + 1);
        data[columnIndex - 1] = value;
    }
}複製程式碼
  • 呼叫 #load() 方法,將當前結果集的一條行資料載入到記憶體。

2.2.3 AbstractDecoratorResultSetMerger

AbstractDecoratorResultSetMerger,裝飾結果集歸併抽象類,通過呼叫其裝飾的歸併物件 #getValue() 方法獲得行資料。

public abstract class AbstractDecoratorResultSetMerger implements ResultSetMerger {

    /**
     * 裝飾的歸併物件
     */
    private final ResultSetMerger resultSetMerger;

    @Override
    public Object getValue(final int columnIndex, final Class<?> type) throws SQLException {
        return resultSetMerger.getValue(columnIndex, type);
    }
}複製程式碼

3. OrderByStreamResultSetMerger

OrderByStreamResultSetMerger,基於 Stream 方式排序歸併結果集實現。

3.1 歸併演算法

因為各個分片結果集已經排序完成,使用《歸併演算法》能夠充分利用這個優勢。

歸併操作(merge),也叫歸併演算法,指的是將兩個已經排序的序列合併成一個序列的操作。歸併排序演算法依賴歸併操作。

【迭代法】

  1. 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
  2. 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
  3. 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
  4. 重複步驟3直到某一指標到達序列尾
  5. 將另一序列剩下的所有元素直接複製到合併序列尾

從定義上看,是不是超級符合我們這個場景。? 此時此刻,你是不是捂著胸口,感嘆:“大學怎麼沒好好學資料結構與演算法呢”?反正我是捂著了,都是眼淚。

public class OrderByStreamResultSetMerger extends AbstractStreamResultSetMerger {

    /**
     * 排序列
     */
    @Getter(AccessLevel.NONE)
    private final List<OrderItem> orderByItems;
    /**
     * 排序值物件佇列
     */
    private final Queue<OrderByValue> orderByValuesQueue;
    /**
     * 預設排序型別
     */
    private final OrderType nullOrderType;
    /**
     * 是否第一個 ResultSet 已經呼叫 #next()
     */
    private boolean isFirstNext;

    public OrderByStreamResultSetMerger(final List<ResultSet> resultSets, final List<OrderItem> orderByItems, final OrderType nullOrderType) throws SQLException {
        this.orderByItems = orderByItems;
        this.orderByValuesQueue = new PriorityQueue<>(resultSets.size());
        this.nullOrderType = nullOrderType;
        orderResultSetsToQueue(resultSets);
        isFirstNext = true;
    }

    private void orderResultSetsToQueue(final List<ResultSet> resultSets) throws SQLException {
        for (ResultSet each : resultSets) {
            OrderByValue orderByValue = new OrderByValue(each, orderByItems, nullOrderType);
            if (orderByValue.next()) {
                orderByValuesQueue.offer(orderByValue);
            }
        }
        // 設定當前 ResultSet,這樣 #getValue() 能拿到記錄
        setCurrentResultSet(orderByValuesQueue.isEmpty() ? resultSets.get(0) : orderByValuesQueue.peek().getResultSet());
    }複製程式碼
  • 屬性 orderByValuesQueue 使用的佇列實現是優先順序佇列( PriorityQueue )。有興趣的同學可以看看《JDK原始碼研究PriorityQueue》,本文不展開講,不是主角戲份不多。我們記住幾個方法的用途:
    • #offer():增加元素。增加時,會將該元素和已有元素們按照優先順序進行排序
    • #peek():獲得優先順序第一的元素
    • #pool():獲得優先順序第一的元素並移除
  • 一個 ResultSet 構建一個 OrderByValue 用於排序,即上文歸併演算法提到的“空間”

      public final class OrderByValue implements Comparable<OrderByValue> {
    
          /**
           * 已排序結果集
           */
          @Getter
          private final ResultSet resultSet;
          /**
           * 排序列
           */
          private final List<OrderItem> orderByItems;
          /**
           * 預設排序型別
           */
          private final OrderType nullOrderType;
          /**
           * 排序列對應的值陣列
           * 因為一條記錄可能有多個排序列,所以是陣列
           */
          private List<Comparable<?>> orderValues;
    
          /**
           * 遍歷下一個結果集遊標.
           * 
           * @return 是否有下一個結果集
           * @throws SQLException SQL異常
           */
          public boolean next() throws SQLException {
              boolean result = resultSet.next();
              orderValues = result ? getOrderValues() : Collections.<Comparable<?>>emptyList();
              return result;
          }
    
          /**
           * 獲得 排序列對應的值陣列
           *
           * @return 排序列對應的值陣列
           * @throws SQLException 當結果集關閉時
           */
          private List<Comparable<?>> getOrderValues() throws SQLException {
              List<Comparable<?>> result = new ArrayList<>(orderByItems.size());
              for (OrderItem each : orderByItems) {
                  Object value = resultSet.getObject(each.getIndex());
                  Preconditions.checkState(null == value || value instanceof Comparable, "Order by value must implements Comparable");
                  result.add((Comparable<?>) value);
              }
              return result;
          }
    
          /**
           * 對比 {@link #orderValues},即兩者的第一條記錄
           *
           * @param o 對比 OrderByValue
           * @return -1 0 1
           */
          @Override
          public int compareTo(final OrderByValue o) {
              for (int i = 0; i < orderByItems.size(); i++) {
                  OrderItem thisOrderBy = orderByItems.get(i);
                  int result = ResultSetUtil.compareTo(orderValues.get(i), o.orderValues.get(i), thisOrderBy.getType(), nullOrderType);
                  if (0 != result) {
                      return result;
                  }
              }
              return 0;
          }
      }複製程式碼
    • 呼叫 OrderByValue#next() 方法時,獲得其對應結果集排在第一條的記錄,通過 #getOrderValues() 計算該記錄的排序欄位值。這樣兩個OrderByValue 通過 #compareTo() 方法可以比較兩個結果集的第一條記錄。
  • if (orderByValue.next()) { 處,呼叫 OrderByValue#next() 後,新增到 PriorityQueue。因此,orderByValuesQueue.peek().getResultSet() 能夠獲得多個 ResultSet 中排在第一的。

3.2 #next()

通過呼叫 OrderByStreamResultSetMerger#next() 不斷獲得當前排在第一的記錄。#next() 每次呼叫後,實際做的是當前 ResultSet 的替換,以及當前的 ResultSet 的記錄指向下一條。這樣說起來可能比較繞,我們來看一張圖:

  • 白色向下箭頭:OrderByStreamResultSetMerger 對 ResultSet 的指向。
  • 黑色箭頭:ResultSet 對當前記錄的指向。
  • ps:這塊如果分享的不清晰讓您費勁,十分抱歉。歡迎加我微信(wangwenbin-server)交流下,這樣我也可以優化表述。
// OrderByStreamResultSetMerger.java
@Override
public boolean next() throws SQLException {
   if (orderByValuesQueue.isEmpty()) {
       return false;
   }
   if (isFirstNext) {
       isFirstNext = false;
       return true;
   }
   // 移除上一次獲得的 ResultSet
   OrderByValue firstOrderByValue = orderByValuesQueue.poll();
   // 如果上一次獲得的 ResultSet還有下一條記錄,繼續新增到 排序值物件佇列
   if (firstOrderByValue.next()) {
       orderByValuesQueue.offer(firstOrderByValue);
   }
   if (orderByValuesQueue.isEmpty()) {
       return false;
   }
   // 設定當前 ResultSet
   setCurrentResultSet(orderByValuesQueue.peek().getResultSet());
   return true;
}複製程式碼
  • orderByValuesQueue.poll() 移除上一次獲得的 ResultSet。為什麼不能 #setCurrentResultSet() 就移除呢?如果該 ResultSet 裡面還存在下一條記錄,需要繼續參加排序。而判斷是否有下一條,需要呼叫 ResultSet#next() 方法,這會導致 ResultSet 指向了下一條記錄。因而 orderByValuesQueue.poll() 呼叫是後置的。
  • isFirstNext 變數那的判斷看著是不是很“靈異”?因為 #orderResultSetsToQueue() 處設定了第一次的 ResultSet。如果不加這個標記,會導致第一條記錄“不見”了。
  • 通過不斷的 Queue#poll()Queue#offset() 實現排序。巧妙!彷彿 Get 新技能了:

      // 移除上一次獲得的 ResultSet
      OrderByValue firstOrderByValue = orderByValuesQueue.poll();
      // 如果上一次獲得的 ResultSet還有下一條記錄,繼續新增到 排序值物件佇列
      if (firstOrderByValue.next()) {
        orderByValuesQueue.offer(firstOrderByValue);
      }複製程式碼

在看下,我們上文 Stream 方式歸併的定義:將資料遊標與結果集的遊標保持一致,順序的從結果集中一條條的獲取正確的資料。是不是能夠清晰的對上了?!?

4. GroupByStreamResultSetMerger

GroupByStreamResultSetMerger,基於 Stream 方式分組歸併結果集實現。 它繼承自 OrderByStreamResultSetMerger,在排序的邏輯上,實現分組功能。實現原理也較為簡單:

public final class GroupByStreamResultSetMerger extends OrderByStreamResultSetMerger {

    /**
     * 查詢列名與位置對映
     */
    private final Map<String, Integer> labelAndIndexMap;
    /**
     * Select SQL語句物件
     */
    private final SelectStatement selectStatement;
    /**
     * 當前結果記錄
     */
    private final List<Object> currentRow;
    /**
     * 下一條結果記錄 GROUP BY 條件
     */
    private List<?> currentGroupByValues;

    public GroupByStreamResultSetMerger(
            final Map<String, Integer> labelAndIndexMap, final List<ResultSet> resultSets, final SelectStatement selectStatement, final OrderType nullOrderType) throws SQLException {
        super(resultSets, selectStatement.getOrderByItems(), nullOrderType);
        this.labelAndIndexMap = labelAndIndexMap;
        this.selectStatement = selectStatement;
        currentRow = new ArrayList<>(labelAndIndexMap.size());
        // 初始化下一條結果記錄 GROUP BY 條件
        currentGroupByValues = getOrderByValuesQueue().isEmpty() ? Collections.emptyList() : new GroupByValue(getCurrentResultSet(), selectStatement.getGroupByItems()).getGroupValues();
    }

    @Override
    public Object getValue(final int columnIndex, final Class<?> type) throws SQLException {
        return currentRow.get(columnIndex - 1);
    }
    @Override
    public Object getValue(final String columnLabel, final Class<?> type) throws SQLException {
        Preconditions.checkState(labelAndIndexMap.containsKey(columnLabel), String.format("Can`t find columnLabel: %s", columnLabel));
        return currentRow.get(labelAndIndexMap.get(columnLabel) - 1);
    }
}複製程式碼
  • currentRow 為當前結果記錄,使用 #getValue()#getCalendarValue() 方法獲得當前結果記錄的查詢列值。
  • currentGroupByValues下一條結果記錄 GROUP BY 條件,通過 GroupByValue 生成:

      public final class GroupByValue {
    
          /**
           * 分組條件值陣列
           */
          private final List<?> groupValues;
    
          public GroupByValue(final ResultSet resultSet, final List<OrderItem> groupByItems) throws SQLException {
              groupValues = getGroupByValues(resultSet, groupByItems);
          }
    
          /**
           * 獲得分組條件值陣列
           * 例如,`GROUP BY user_id, order_status` 返回的某條記錄結果為 `userId = 1, order_status = 3`,對應的 `groupValues = [1, 3]`
           * @param resultSet 結果集(單分片)
           * @param groupByItems 分組列
           * @return 分組條件值陣列
           * @throws SQLException 當結果集關閉
           */
          private List<?> getGroupByValues(final ResultSet resultSet, final List<OrderItem> groupByItems) throws SQLException {
              List<Object> result = new ArrayList<>(groupByItems.size());
              for (OrderItem each : groupByItems) {
                  result.add(resultSet.getObject(each.getIndex())); // 從結果集獲得每個分組條件的值
              }
              return result;
          }
      }複製程式碼
  • GroupByStreamResultSetMerger 在建立時,當前結果記錄實際未合併,需要先呼叫 #next(),在使用 #getValue() 等方法獲取值,這個和 OrderByStreamResultSetMerger 不同,可能是個 BUG。

4.1 AggregationUnit

AggregationUnit,歸併計算單元介面,有兩個介面方法:

  • #merge():歸併聚合值
  • #getResult():獲取計算結果

一共有三個實現類:

實現都比較易懂,直接點選連結檢視原始碼,我們就不浪費篇幅貼程式碼啦。

4.2 #next()

我們先看看大體的呼叫流程:

? 看起來程式碼比較多,邏輯其實比較清晰,對照著順序圖順序往下讀即可。

// GroupByStreamResultSetMerger.java
@Override
public boolean next() throws SQLException {
   // 清除當前結果記錄
   currentRow.clear();
   if (getOrderByValuesQueue().isEmpty()) {
       return false;
   }
   //
   if (isFirstNext()) {
       super.next();
   }
   // 順序合併下面相同分組條件的記錄
   if (aggregateCurrentGroupByRowAndNext()) {
       // 生成下一條結果記錄 GROUP BY 條件
       currentGroupByValues = new GroupByValue(getCurrentResultSet(), selectStatement.getGroupByItems()).getGroupValues();
   }
   return true;
}

private boolean aggregateCurrentGroupByRowAndNext() throws SQLException {
   boolean result = false;
   // 生成計算單元
   Map<AggregationSelectItem, AggregationUnit> aggregationUnitMap = Maps.toMap(selectStatement.getAggregationSelectItems(), new Function<AggregationSelectItem, AggregationUnit>() {

       @Override
       public AggregationUnit apply(final AggregationSelectItem input) {
           return AggregationUnitFactory.create(input.getType());
       }
   });
   // 迴圈順序合併下面相同分組條件的記錄
   while (currentGroupByValues.equals(new GroupByValue(getCurrentResultSet(), selectStatement.getGroupByItems()).getGroupValues())) {
       // 歸併聚合值
       aggregate(aggregationUnitMap);
       // 快取當前記錄到結果記錄
       cacheCurrentRow();
       // 獲取下一條記錄
       result = super.next();
       if (!result) {
           break;
       }
   }
   // 設定當前記錄的聚合欄位結果
   setAggregationValueToCurrentRow(aggregationUnitMap);
   return result;
}

private void aggregate(final Map<AggregationSelectItem, AggregationUnit> aggregationUnitMap) throws SQLException {
   for (Entry<AggregationSelectItem, AggregationUnit> entry : aggregationUnitMap.entrySet()) {
       List<Comparable<?>> values = new ArrayList<>(2);
       if (entry.getKey().getDerivedAggregationSelectItems().isEmpty()) { // SUM/COUNT/MAX/MIN 聚合列
           values.add(getAggregationValue(entry.getKey()));
       } else {
           for (AggregationSelectItem each : entry.getKey().getDerivedAggregationSelectItems()) { // AVG 聚合列
               values.add(getAggregationValue(each));
           }
       }
       entry.getValue().merge(values);
   }
}

private void cacheCurrentRow() throws SQLException {
   for (int i = 0; i < getCurrentResultSet().getMetaData().getColumnCount(); i++) {
       currentRow.add(getCurrentResultSet().getObject(i + 1));
   }
}

private Comparable<?> getAggregationValue(final AggregationSelectItem aggregationSelectItem) throws SQLException {
   Object result = getCurrentResultSet().getObject(aggregationSelectItem.getIndex());
   Preconditions.checkState(null == result || result instanceof Comparable, "Aggregation value must implements Comparable");
   return (Comparable<?>) result;
}

private void setAggregationValueToCurrentRow(final Map<AggregationSelectItem, AggregationUnit> aggregationUnitMap) {
   for (Entry<AggregationSelectItem, AggregationUnit> entry : aggregationUnitMap.entrySet()) {
       currentRow.set(entry.getKey().getIndex() - 1, entry.getValue().getResult()); // 獲取計算結果
   }
}複製程式碼

5. GroupByMemoryResultSetMerger

GroupByMemoryResultSetMerger,基於 記憶體 分組歸併結果集實現。

區別於 GroupByStreamResultSetMerger,其無法使用每個分片結果集的有序的特點,只能在記憶體中合併後,進行整個重新排序。因而,效能和記憶體都較 GroupByStreamResultSetMerger 會差。

主流程如下:

public final class GroupByMemoryResultSetMerger extends AbstractMemoryResultSetMerger {

    /**
     * Select SQL語句物件
     */
    private final SelectStatement selectStatement;
    /**
     * 預設排序型別
     */
    private final OrderType nullOrderType;
    /**
     * 記憶體結果集
     */
    private final Iterator<MemoryResultSetRow> memoryResultSetRows;

    public GroupByMemoryResultSetMerger(
            final Map<String, Integer> labelAndIndexMap, final List<ResultSet> resultSets, final SelectStatement selectStatement, final OrderType nullOrderType) throws SQLException {
        super(labelAndIndexMap);
        this.selectStatement = selectStatement;
        this.nullOrderType = nullOrderType;
        memoryResultSetRows = init(resultSets);
    }

    private Iterator<MemoryResultSetRow> init(final List<ResultSet> resultSets) throws SQLException {
        Map<GroupByValue, MemoryResultSetRow> dataMap = new HashMap<>(1024); // 分組條件值與記憶體記錄對映
        Map<GroupByValue, Map<AggregationSelectItem, AggregationUnit>> aggregationMap = new HashMap<>(1024); // 分組條件值與聚合列對映
        // 遍歷結果集
        for (ResultSet each : resultSets) {
            while (each.next()) {
                // 生成分組條件
                GroupByValue groupByValue = new GroupByValue(each, selectStatement.getGroupByItems());
                // 初始化分組條件到 dataMap、aggregationMap 對映
                initForFirstGroupByValue(each, groupByValue, dataMap, aggregationMap);
                // 歸併聚合值
                aggregate(each, groupByValue, aggregationMap);
            }
        }
        // 設定聚合列結果到記憶體記錄
        setAggregationValueToMemoryRow(dataMap, aggregationMap);
        // 記憶體排序
        List<MemoryResultSetRow> result = getMemoryResultSetRows(dataMap);
        // 設定當前 ResultSet,這樣 #getValue() 能拿到記錄
        if (!result.isEmpty()) {
            setCurrentResultSetRow(result.get(0));
        }
        return result.iterator();
    }
}複製程式碼
  • #initForFirstGroupByValue() 初始化分組條件dataMapaggregationMap 對映中,這樣可以呼叫 #aggregate() 將聚合值歸併到 aggregationMap 裡的該分組條件。

     private void initForFirstGroupByValue(final ResultSet resultSet, final GroupByValue groupByValue, final Map<GroupByValue, MemoryResultSetRow> dataMap, 
                                            final Map<GroupByValue, Map<AggregationSelectItem, AggregationUnit>> aggregationMap) throws SQLException {
          // 初始化分組條件到 dataMap
          if (!dataMap.containsKey(groupByValue)) {
              dataMap.put(groupByValue, new MemoryResultSetRow(resultSet));
          }
          // 初始化分組條件到 aggregationMap
          if (!aggregationMap.containsKey(groupByValue)) {
              Map<AggregationSelectItem, AggregationUnit> map = Maps.toMap(selectStatement.getAggregationSelectItems(), new Function<AggregationSelectItem, AggregationUnit>() {
    
                  @Override
                  public AggregationUnit apply(final AggregationSelectItem input) {
                      return AggregationUnitFactory.create(input.getType());
                  }
              });
              aggregationMap.put(groupByValue, map);
          }
      }複製程式碼
  • 聚合完每個分組條件後,將聚合列結果 aggregationMap 合併到 dataMap

      private void setAggregationValueToMemoryRow(final Map<GroupByValue, MemoryResultSetRow> dataMap, final Map<GroupByValue, Map<AggregationSelectItem, AggregationUnit>> aggregationMap) {
         for (Entry<GroupByValue, MemoryResultSetRow> entry : dataMap.entrySet()) { // 遍 歷記憶體記錄
             for (AggregationSelectItem each : selectStatement.getAggregationSelectItems()) { // 遍歷 每個聚合列
                 entry.getValue().setCell(each.getIndex(), aggregationMap.get(entry.getKey()).get(each).getResult());
             }
         }
      }複製程式碼
  • 呼叫 #getMemoryResultSetRows() 方法對記憶體記錄進行記憶體排序

// GroupByMemoryResultSetMerger.java
private List<MemoryResultSetRow> getMemoryResultSetRows(final Map<GroupByValue, MemoryResultSetRow> dataMap) {
   List<MemoryResultSetRow> result = new ArrayList<>(dataMap.values());
   Collections.sort(result, new GroupByRowComparator(selectStatement, nullOrderType)); // 記憶體排序
   return result;
}

// GroupByRowComparator.java
private int compare(final MemoryResultSetRow o1, final MemoryResultSetRow o2, final List<OrderItem> orderItems) {
   for (OrderItem each : orderItems) {
       Object orderValue1 = o1.getCell(each.getIndex());
       Preconditions.checkState(null == orderValue1 || orderValue1 instanceof Comparable, "Order by value must implements Comparable");
       Object orderValue2 = o2.getCell(each.getIndex());
       Preconditions.checkState(null == orderValue2 || orderValue2 instanceof Comparable, "Order by value must implements Comparable");
       int result = ResultSetUtil.compareTo((Comparable) orderValue1, (Comparable) orderValue2, each.getType(), nullOrderType);
       if (0 != result) {
           return result;
       }
   }
   return 0;
}複製程式碼
  • 總的來說,GROUP BY 記憶體歸併和我們日常使用 Map 計算使用者訂單數是比較相似的。

5.1 #next()

@Override
public boolean next() throws SQLException {
   if (memoryResultSetRows.hasNext()) {
       setCurrentResultSetRow(memoryResultSetRows.next());
       return true;
   }
   return false;
}複製程式碼
  • 記憶體歸併完成後,使用 memoryResultSetRows 不斷獲得下一條記錄。

6. IteratorStreamResultSetMerger

IteratorStreamResultSetMerger,基於 Stream 迭代歸併結果集實現。

public final class IteratorStreamResultSetMerger extends AbstractStreamResultSetMerger {

    /**
     * ResultSet 陣列迭代器
     */
    private final Iterator<ResultSet> resultSets;

    public IteratorStreamResultSetMerger(final List<ResultSet> resultSets) {
        this.resultSets = resultSets.iterator();
        // 設定當前 ResultSet,這樣 #getValue() 能拿到記錄
        setCurrentResultSet(this.resultSets.next());
    }

    @Override
    public boolean next() throws SQLException {
        // 當前 ResultSet 迭代下一條記錄
        if (getCurrentResultSet().next()) {
            return true;
        }
        if (!resultSets.hasNext()) {
            return false;
        }
        // 獲得下一個ResultSet, 設定當前 ResultSet
        setCurrentResultSet(resultSets.next());
        boolean hasNext = getCurrentResultSet().next();
        if (hasNext) {
            return true;
        }
        while (!hasNext && resultSets.hasNext()) {
            setCurrentResultSet(resultSets.next());
            hasNext = getCurrentResultSet().next();
        }
        return hasNext;
    }
}複製程式碼

7. LimitDecoratorResultSetMerger

LimitDecoratorResultSetMerger,基於 Decorator 分頁結果集歸併實現。

public final class LimitDecoratorResultSetMerger extends AbstractDecoratorResultSetMerger {

    /**
     * 分頁條件
     */
    private final Limit limit;
    /**
     * 是否全部記錄都跳過了,即無符合條件記錄
     */
    private final boolean skipAll;
    /**
     * 當前已返回行數
     */
    private int rowNumber;

    public LimitDecoratorResultSetMerger(final ResultSetMerger resultSetMerger, final Limit limit) throws SQLException {
        super(resultSetMerger);
        this.limit = limit;
        skipAll = skipOffset();
    }

    private boolean skipOffset() throws SQLException {
        // 跳過 skip 記錄
        for (int i = 0; i < limit.getOffsetValue(); i++) {
            if (!getResultSetMerger().next()) {
                return true;
            }
        }
        // 行數
        rowNumber = limit.isRowCountRewriteFlag() ? 0 : limit.getOffsetValue();
        return false;
    }

    @Override
    public boolean next() throws SQLException {
        if (skipAll) {
            return false;
        }
        // 獲得下一條記錄
        if (limit.getRowCountValue() > -1) {
            return ++rowNumber <= limit.getRowCountValue() && getResultSetMerger().next();
        }
        // 部分db 可以直 offset,不寫 limit 行數,例如 oracle
        return getResultSetMerger().next();
    }

}複製程式碼
  • LimitDecoratorResultSetMerger 可以對其他 ResultSetMerger 進行裝飾,呼叫其他 ResultSetMerger 的 #next() 不斷獲得下一條記錄。

666. 彩蛋

誒?應該是有蠻多地方解釋的不是很清晰,如果讓您閱讀誤解或是阻塞,非常抱歉。程式碼讀起來比較易懂,使用文字來解釋,對錶述能力較差的自己,可能就絞盡腦汁,一臉懵逼。

恩,如果可以,還煩請把讀起來不太爽的地方告訴我,謝謝。

厚著臉皮,道友,分享一波朋友圈可好?

如下是小禮包,嘿嘿

歸併結果集介面 SQL
OrderByStreamResultSetMerger SELECT * FROM t_order ORDER BY id
GroupByStreamResultSetMerger SELECT uid, AVG(id) FROM t_order GROUP BY uid
GroupByMemoryResultSetMerger SELECT uid FROM t_order GROUP BY id ORDER BY id DESC
IteratorStreamResultSetMerger SELECT * FROM t_order
LimitDecoratorResultSetMerger SELECT * FROM t_order ORDER BY id LIMIT 10

相關文章