面試官:如何在千萬級數據中查詢 10W 的數據,都有什麼方案?

来源:https://www.cnblogs.com/javastack/archive/2023/03/13/17211490.html
-Advertisement-
Play Games

作者:變速風聲 鏈接:https://juejin.cn/post/7104090532015505416 前言 在開發中遇到一個業務訴求,需要在千萬量級的底池數據中篩選出不超過 10W 的數據,並根據配置的權重規則進行排序、打散(如同一個類目下的商品數據不能連續出現 3 次)。 下麵對該業務訴求的 ...


作者:變速風聲
鏈接:https://juejin.cn/post/7104090532015505416

前言

在開發中遇到一個業務訴求,需要在千萬量級的底池數據中篩選出不超過 10W 的數據,並根據配置的權重規則進行排序、打散(如同一個類目下的商品數據不能連續出現 3 次)。

下麵對該業務訴求的實現,設計思路和方案優化進行介紹,對「千萬量級數據中查詢 10W 量級的數據」設計瞭如下方案

  1. 多線程 + CK 翻頁方案
  2. ES scroll scan 深翻頁方案
  3. ES + Hbase 組合方案
  4. RediSearch + RedisJSON 組合方案

初版設計方案

整體方案設計為:

  1. 先根據配置的「篩選規則」,從底池表中篩選出「目標數據」
  2. 在根據配置的「排序規則」,對「目標數據」進行排序,得到「結果數據」

技術方案如下:

  1. 每天運行導數任務,把現有的千萬量級的底池數據(Hive 表)導入到 Clickhouse 中,後續使用 CK 表進行數據篩選。
  2. 將業務配置的篩選規則和排序規則,構建為一個「篩選 + 排序」對象 SelectionQueryCondition
  3. 從 CK 底池表取「目標數據」時,開啟多線程,進行分頁篩選,將獲取到的「目標數據」存放到 result 列表中。
//分頁大小  預設 5000
int pageSize = this.getPageSize();
//頁碼數
int pageCnt = totalNum / this.getPageSize() + 1;

List<Map<String, Object>> result = Lists.newArrayList();
List<Future<List<Map<String, Object>>>> futureList = new ArrayList<>(pageCnt);

//開啟多線程調用
for (int i = 1; i <= pageCnt; i++) {
    //將業務配置的篩選規則和排序規則 構建為 SelectionQueryCondition 對象
    SelectionQueryCondition selectionQueryCondition = buildSelectionQueryCondition(selectionQueryRuleData);
    selectionQueryCondition.setPageSize(pageSize);
    selectionQueryCondition.setPage(i);
    futureList.add(selectionQueryEventPool.submit(new QuerySelectionDataThread(selectionQueryCondition)));
}

for (Future<List<Map<String, Object>>> future : futureList) {
    //RPC 調用
    List<Map<String, Object>> queryRes = future.get(20, TimeUnit.SECONDS);
    if (CollectionUtils.isNotEmpty(queryRes)) {
        // 將目標數據存放在 result 中
        result.addAll(queryRes);
    }
}

對目標數據 result 進行排序,得到最終的「結果數據」。

推薦一個開源免費的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

CK分頁查詢

在「初版設計方案」章節的第 3 步提到了「從 CK 底池表取目標數據時,開啟多線程,進行分頁篩選」。此處對 CK 分頁查詢進行介紹。

封裝了 queryPoolSkuList 方法,負責從 CK 表中獲得目標數據。該方法內部調用了 sqlSession.selectList 方法。

public List<Map<String, Object>> queryPoolSkuList( Map<String, Object> params ) {
    List<Map<String, Object>> resultMaps = new ArrayList<>();

    QueryCondition queryCondition = parseQueryCondition(params);
    List<Map<String, Object>> mapList = lianNuDao.queryPoolSkuList(getCkDt(),queryCondition);
    if (CollectionUtils.isNotEmpty(mapList)) {
        for (Map<String,Object> data : mapList) {
            resultMaps.add(camelKey(data));
        }
    }
    return resultMaps;
}

// lianNuDao.queryPoolSkuList

@Autowired
@Qualifier("ckSqlNewSession")
private SqlSession sqlSession;

public List<Map<String, Object>> queryPoolSkuList( String dt, QueryCondition queryCondition ) {
    queryCondition.setDt(dt);
    queryCondition.checkMultiQueryItems();
    return sqlSession.selectList("LianNu.queryPoolSkuList",queryCondition);
}

sqlSession.selectList 方法中調用了和 CK 交互的 queryPoolSkuList 查詢方法,部分代碼如下。

<select id="queryPoolSkuList" parameterType="com.jd.bigai.domain.liannu.QueryCondition" resultType="java.util.Map">
    select sku_pool_id,i
    tem_sku_id,
    skuPoolName,
    price,
    ...
    ...
    businessType
    from liannu_sku_pool_indicator_all
    where
    dt=#{dt}
    and
    <foreach collection="queryItems" separator=" and " item="queryItem" open=" " close=" " >
        <choose>
            <when test="queryItem.type == 'equal'">
                ${queryItem.field} = #{queryItem.value}
            </when>
            ...
            ...
        </choose>
    </foreach>
    <if test="orderBy == null">
        group by sku_pool_id,item_sku_id
    </if>
    <if test="orderBy != null">
        group by sku_pool_id,item_sku_id,${orderBy} order by ${orderBy} ${orderAd}
    </if>
    <if test="limitEnd != 0">
        limit #{limitStart},#{limitEnd}
    </if>
</select>

可以看到,在 CK 分頁查詢時,是通過 limit #{limitStart},#{limitEnd} 實現的分頁。

limit 分頁方案,在「深翻頁」時會存在性能問題。初版方案上線後,在 1000W 量級的底池數據中篩選 10W 的數據,最壞耗時會達到 10s~18s 左右。

使用ES Scroll Scan 優化深翻頁

對於 CK 深翻頁時候的性能問題,進行了優化,使用 Elasticsearch 的 scroll scan 翻頁方案進行優化。

ES的翻頁方案

ES 翻頁,有下麵幾種方案

  1. from + size 翻頁
  2. scroll 翻頁
  3. scroll scan 翻頁
  4. search after 翻頁
翻頁方式 性能 優點 缺點 場景
from + size 靈活性好,實現簡單 深度分頁問題 數據量比較小,能容忍深度分頁問題
scroll 解決了深度分頁問題 需要維護一個 scrollId(快照版本),無法反應數據的實時性;可排序,但無法跳頁查詢 查詢海量數據
scroll scan 基於 scroll 方案,進一步提升了海量數據查詢的性能 無法排序,其餘缺點同 scroll 查詢海量數據
search after 性能最好,不存在深度分頁問題,能夠反映數據的實時變更 實現複雜,需要有一個全局唯一的欄位。連續分頁的實現會比較複雜,因為每一次查詢都需要上次查詢的結果 不適用於大幅度跳頁查詢,適用於海量數據的分頁

對上述幾種翻頁方案,查詢不同數目的數據,耗時數據如下表。

ES 翻頁方式 1-10 49000-49010 99000-99010
from + size 8ms 30ms 117ms
scroll 7ms 66ms 36ms
search_after 5ms 8ms 7ms

耗時數據

此處,分別使用 Elasticsearch 的 scroll scan 翻頁方案、初版中的 CK 翻頁方案進行數據查詢,對比其耗時數據。

如上測試數據,可以發現,以十萬,百萬,千萬量級的底池為例

  1. 底池量級越大,查詢相同的數據量,耗時越大
  2. 查詢結果 3W 以下時,ES 性能優;查詢結果 5W 以上時,CK 多線程性能優

ES+Hbase組合查詢方案

在「使用 ES Scroll Scan 優化深翻頁」中,使用 Elasticsearch 的 scroll scan 翻頁方案對深翻頁問題進行了優化,但在實現時為單線程調用,所以最終測試耗時數據並不是特別理想,和 CK 翻頁方案性能差不多。

在調研階段發現,從底池中取出 10W 的目標數據時,一個商品包含多個欄位的信息(CK 表中一行記錄有 150 個欄位信息),如價格、會員價、學生價、庫存、好評率等。對於一行記錄,當減少獲取欄位的個數時,查詢耗時會有明顯下降。如對 sku1的商品,從之前獲取價格、會員價、學生價、親友價、庫存等 100 個欄位信息,縮減到只獲取價格、庫存這兩個欄位信息。

如下圖所示,使用 ES 查詢方案,對查詢同樣條數的場景(從千萬級底池中篩選出 7W+ 條數據),獲取的每條記錄的欄位個數從 32 縮減到 17,再縮減到 1個(其實是兩個欄位,一個是商品唯一標識 sku_id,另一個是 ES 對每條文檔記錄的 doc_id)時,查詢的耗時會從 9.3s 下降到 4.2s,再下降到 2.4s。

從中可以得出如下結論

  1. 一次 ES 查詢中,若查詢欄位和信息較多,fetch 階段的耗時,遠大於 query 階段的耗時。
  2. 一次 ES 查詢中,若查詢欄位和信息較多,通過減少不必要的查詢欄位,可以顯著縮短查詢耗時。

下麵對結論中涉及的 queryfetch 查詢階段進行補充說明。

ES查詢的兩個階段:query和fetch

在 ES 中,搜索一般包括兩個階段,queryfetch 階段

query 階段

  • 根據查詢條件,確定要取哪些文檔(doc),篩選出文檔 ID(doc_id

fetch 階段

  • 根據 query 階段返回的文檔 ID(doc_id),取出具體的文檔(doc

ES的filesystem cache

  • ES 會將磁碟中的數據自動緩存到 filesystem cache,在記憶體中查找,提升了速度
  • filesystem cache 無法容納索引數據文件,則會基於磁碟查找,此時查詢速度會明顯變慢
  • 若數量兩過大,基於「ES 查詢的的 query 和 fetch 兩個階段」,可使用 ES + HBase 架構,保證 ES 的數據量小於 filesystem cache,保證查詢速度

組合使用Hbase

在上文調研的基礎上,發現「減少不必要的查詢展示欄位」可以明顯縮短查詢耗時。沿著這個優化思路,參照參考鏈接 ref-1,設計了一種新的查詢方案

  1. ES 僅用於條件篩選,ES 的查詢結果僅包含記錄的唯一標識 sku_id(其實還包含 ES 為每條文檔記錄的 doc_id
  2. Hbase 是列存儲資料庫,每列數據有一個 rowKey。利用 rowKey 篩選一條記錄時,複雜度為 O(1)。(類似於從 HashMap 中根據 keyvalue
  3. 根據 ES 查詢返回的唯一標識 sku_id,作為 Hbase 查詢中的 rowKey,在 O(1) 複雜度下獲取其他信息欄位,如價格,庫存等。

使用 ES + Hbase 組合查詢方案,線上上進行了小規模的灰度測試。在 1000W 量級的底池數據中篩選 10W 的數據,對比 CK 翻頁方案,最壞耗時從 10~18s 優化到了 3~6s 左右。

也應該看到,使用 ES + Hbase 組合查詢方案,會增加系統複雜度,同時數據也需要同時存儲到 ES 和 Hbase。

RediSearch+RedisJSON優化方案

RediSearch 是基於 Redis 構建的分散式全文搜索和聚合引擎,能以極快的速度在 Redis 數據集上執行複雜的搜索查詢。RedisJSON 是一個 Redis 模塊,在 Redis 中提供 JSON 支持。RedisJSON 可以和 RediSearch 無縫配合,實現索引和查詢 JSON 文檔。

根據一些參考資料,RediSearch + RedisJSON 可以實現極高的性能,可謂碾壓其他 NoSQL 方案。在後續版本迭代中,可考慮使用該方案來進一步優化。

下麵給出 RediSearch + RedisJSON 的部分性能數據。

RediSearch 性能數據

在同等伺服器配置下索引了 560 萬個文檔 (5.3GB),RediSearch 構建索引的時間為 221 秒,而 Elasticsearch 為 349 秒。RediSearch 比 ES 快了 58%。

數據建立索引後,使用 32 個客戶端對兩個單詞進行檢索,RediSearch 的吞吐量達到 12.5K ops/sec,ES 的吞吐量為 3.1K ops/sec,RediSearch 比ES 要快 4 倍。同時,RediSearch 的延遲為 8ms,而 ES 為 10ms,RediSearch 延遲稍微低些。

對比 Redisearch Elasticsearch
搜索引擎 專用引擎 基於 Lucene 引擎
編程語言 C 語言 Java
存儲方案 記憶體 磁碟
協議 Redis 序列化協議 HTTP
集群 企業版支持 支持
性能 簡單查詢高於 ES 複雜查詢時高於 RediSearch

RedisJSON 性能數據

根據官網的性能測試報告,RedisJson + RedisSearch 可謂碾壓其他 NoSQL

  • 對於隔離寫入(isolated writes),RedisJSON 比 MongoDB 快 5.4 倍,比 ES 快 200 倍以上
  • 對於隔離讀取(isolated reads),RedisJSON 比 MongoDB 快 12.7 倍,比 ES 快 500 倍以上

在混合工作負載場景中,實時更新不會影響 RedisJSON 的搜索和讀取性能,而 ES 會受到影響。

  • RedisJSON 支持的操作數/秒比 MongoDB 高約 50 倍,比 ES 高 7 倍/秒。
  • RedisJSON 的延遲比 MongoDB 低約 90 倍,比 ES 低 23.7 倍。

此外,RedisJSON 的讀取、寫入和負載搜索延遲,在更高的百分位數中遠比 ES 和 MongoDB 穩定。當增加寫入比率時,RedisJSON 還能處理越來越高的整體吞吐量。而當寫入比率增加時,ES 會降低它可以處理的整體吞吐量。

總結

本文從一個業務訴求觸發,對「千萬量級數據中查詢 10W 量級的數據」介紹了不同的設計方案。對於「在 1000W 量級的底池數據中篩選 10W 的數據」的場景,不同方案的耗時如下

  1. 多線程 + CK 翻頁方案,最壞耗時為 10s~18s
  2. 單線程 + ES scroll scan 深翻頁方案,相比 CK 方案,並未見到明顯優化
  3. ES + Hbase 組合方案,最壞耗時優化到了 3s~6s
  4. RediSearch + RedisJSON 組合方案,後續會實測該方案的耗時

參考資料:

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 首先理解常亮表達式。常量表達式是指值不會改變,並且在編譯過程就能計算得到結果。 const修飾的對象無法修改,constexpr對象在編譯期間就確定且無法修改。 constexpr變數,編譯器在編譯階段驗證變數是否為一個常量表達式。 constexpr側重變數初值編譯階段確定,且無法修改。如果認定變 ...
  • 處理GB/T4754—2017國民經濟行業分類與代碼數據,劃分四級分類存入mysql資料庫【文末獲取下載方式】 第二張圖是之前的格式,今天應一位網友的要求,將其處理為如下第三張圖的格式。更貼近源文檔,方便使用。 如圖所示: 按 門類編碼 門類名稱 大類編碼 大類名稱 中類編碼 中類名稱 小類編碼 小 ...
  • 單元測試、反射 一、單元測試 1.1 單元測試快速入門 所謂單元測試,就是針對最小的功能單元,編寫測試代碼對其進行正確性測試。 我們想想,咱們之前是怎麼進行測試的呢? 比如說我們寫了一個學生管理系統,有添加學生、修改學生、刪除學生、查詢學生等這些功能。要對這些功能這幾個功能進行測試,我們是在main ...
  • 此處為一個測試項目的簡單結構,為了便於管理和使用,所以直接將需要的jar放到lib(名字隨意)文件夾下 方法一、 找到項目結構視窗並打開→打開後按圖片上順序進行點擊→而後找到自己的jar包後選中並確定 以上步驟都結束以後就可以調用包中的方法了 方法二、 打開項目結構管理以後→點擊Libraries→ ...
  • 作者:你呀不牛 鏈接:https://juejin.cn/post/7114669787870920734 前段時間,同事在代碼中KW掃描的時候出現這樣一條: 上面出現這樣的原因是在使用foreach對HashMap進行遍歷時,同時進行put賦值操作會有問題,異常ConcurrentModifica ...
  • 1.總結內容參考:https://blog.csdn.net/dhklsl/article/details/127533485 2.下麵是本人工作項目中實戰到的案例(具體業務的實體沒必要關註) 2.1.攔截器使用 點擊查看代碼 import java.lang.invoke.MethodHandle ...
  • IDEA: 如何導入項目模塊 以及 將 Java程式打包 JAR 詳細步驟 、 @ IDEA 導入項目模塊 Module 一. 創建一個空項目 想要導入模塊 Module ,我們需要先創建一個項目,因為 Module模塊在 IDEA 中是存在於項目下的。 這裡我們先創建一個空項目,當然已經有項目了, ...
  • 我這個人比較懶,總是喜歡把收到的重要文件,或者比較緊急的文件放到桌面,久而久之,桌面或者文件夾越來越亂 。 不知道大家是不是像我一樣的 我滴媽呀,看著就很崩潰! 之所以放在桌面上,主要是為了下次使用的時候好找 但是,其實,結果…並沒有 結果,我的馬馬~~ 反而更難找了 也不知道越亂越好找這句話是誰第 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:本文代碼示例演示瞭如何在WPF中使用LiveCharts庫創建動態條形圖。通過創建數據模型、ViewModel和在XAML中使用`CartesianChart`控制項,你可以輕鬆實現圖表的數據綁定和動態更新。我將通過清晰的步驟指南包括詳細的中文註釋,幫助你快速理解並應用這一功能。 先上效果: 在 ...
  • openGauss(GaussDB ) openGauss是一款全面友好開放,攜手伙伴共同打造的企業級開源關係型資料庫。openGauss採用木蘭寬鬆許可證v2發行,提供面向多核架構的極致性能、全鏈路的業務、數據安全、基於AI的調優和高效運維的能力。openGauss深度融合華為在資料庫領域多年的研 ...
  • openGauss(GaussDB ) openGauss是一款全面友好開放,攜手伙伴共同打造的企業級開源關係型資料庫。openGauss採用木蘭寬鬆許可證v2發行,提供面向多核架構的極致性能、全鏈路的業務、數據安全、基於AI的調優和高效運維的能力。openGauss深度融合華為在資料庫領域多年的研 ...
  • 概述:本示例演示了在WPF應用程式中實現多語言支持的詳細步驟。通過資源字典和數據綁定,以及使用語言管理器類,應用程式能夠在運行時動態切換語言。這種方法使得多語言支持更加靈活,便於維護,同時提供清晰的代碼結構。 在WPF中實現多語言的一種常見方法是使用資源字典和數據綁定。以下是一個詳細的步驟和示例源代 ...
  • 描述(做一個簡單的記錄): 事件(event)的本質是一個委托;(聲明一個事件: public event TestDelegate eventTest;) 委托(delegate)可以理解為一個符合某種簽名的方法類型;比如:TestDelegate委托的返回數據類型為string,參數為 int和 ...
  • 1、AOT適合場景 Aot適合工具類型的項目使用,優點禁止反編 ,第一次啟動快,業務型項目或者反射多的項目不適合用AOT AOT更新記錄: 實實在在經過實踐的AOT ORM 5.1.4.117 +支持AOT 5.1.4.123 +支持CodeFirst和非同步方法 5.1.4.129-preview1 ...
  • 總說周知,UWP 是運行在沙盒裡面的,所有許可權都有嚴格限制,和沙盒外交互也需要特殊的通道,所以從根本杜絕了 UWP 毒瘤的存在。但是實際上 UWP 只是一個應用模型,本身是沒有什麼許可權管理的,許可權管理全靠 App Container 沙盒控制,如果我們脫離了這個沙盒,UWP 就會放飛自我了。那麼有沒... ...
  • 目錄條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)限制類型和值規定能做和不能做的事提供行為一致的介面條款19:設計class猶如設計type(Treat class de ...
  • title: 從零開始:Django項目的創建與配置指南 date: 2024/5/2 18:29:33 updated: 2024/5/2 18:29:33 categories: 後端開發 tags: Django WebDev Python ORM Security Deployment Op ...
  • 1、BOM對象 BOM:Broswer object model,即瀏覽器提供我們開發者在javascript用於操作瀏覽器的對象。 1.1、window對象 視窗方法 // BOM Browser object model 瀏覽器對象模型 // js中最大的一個對象.整個瀏覽器視窗出現的所有東西都 ...