最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網 會員登陸 & 注冊

如何在千萬級數據中查詢 10W 的數據,都有什么方案?

2023-03-14 10:37 作者:程序員-王堅  | 我要投稿

前言

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

下面對該業(yè)務訴求的實現,設計思路和方案優(yōu)化進行介紹,對「千萬量級數據中查詢 10W 量級的數據」設計了如下方案

  1. 多線程 + CK 翻頁方案

  2. ES?scroll scan?深翻頁方案

  3. ES + Hbase 組合方案

  4. RediSearch + RedisJSON 組合方案

初版設計方案

整體方案設計為:

  1. 先根據配置的「篩選規(guī)則」,從底池表中篩選出「目標數據」

  2. 在根據配置的「排序規(guī)則」,對「目標數據」進行排序,得到「結果數據」

技術方案如下:

  1. 每天運行導數任務,把現有的千萬量級的底池數據(Hive?表)導入到 Clickhouse 中,后續(xù)使用 CK 表進行數據篩選。

  2. 將業(yè)務配置的篩選規(guī)則和排序規(guī)則,構建為一個「篩選 + 排序」對象?SelectionQueryCondition

  3. 從 CK 底池表取「目標數據」時,開啟多線程,進行分頁篩選,將獲取到的「目標數據」存放到?result?列表中。

//分頁大小 ?默認 5000int 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++) { ? ?//將業(yè)務配置的篩選規(guī)則和排序規(guī)則 構建為 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分頁查詢

在「初版設計方案」章節(jié)的第 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 優(yōu)化深翻頁

對于 CK 深翻頁時候的性能問題,進行了優(yōu)化,使用 Elasticsearch 的?scroll scan?翻頁方案進行優(yōu)化。

ES的翻頁方案

ES 翻頁,有下面幾種方案

  1. from?+?size?翻頁

  2. scroll?翻頁

  3. scroll scan?翻頁

  4. search after?翻頁

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

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

ES 翻頁方式1-1049000-4901099000-99010from + size8ms30ms117msscroll7ms66ms36mssearch_after5ms8ms7ms

耗時數據

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


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

  1. 底池量級越大,查詢相同的數據量,耗時越大

  2. 查詢結果 3W 以下時,ES 性能優(yōu);查詢結果 5W 以上時,CK 多線程性能優(yōu)

ES+Hbase組合查詢方案

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

在調研階段發(fā)現,從底池中取出 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 查詢中,若查詢字段和信息較多,通過減少不必要的查詢字段,可以顯著縮短查詢耗時。

下面對結論中涉及的?query?和?fetch?查詢階段進行補充說明。

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

在 ES 中,搜索一般包括兩個階段,query?和?fetch?階段

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

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

  1. ES 僅用于條件篩選,ES 的查詢結果僅包含記錄的唯一標識?sku_id(其實還包含 ES 為每條文檔記錄的?doc_id

  2. Hbase 是列存儲數據庫,每列數據有一個?rowKey。利用?rowKey?篩選一條記錄時,復雜度為?O(1)。(類似于從?HashMap?中根據?key?取?value

  3. 根據 ES 查詢返回的唯一標識?sku_id,作為 Hbase 查詢中的?rowKey,在?O(1)?復雜度下獲取其他信息字段,如價格,庫存等。

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

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

RediSearch+RedisJSON優(yōu)化方案

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

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

下面給出?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 延遲稍微低些。

對比RedisearchElasticsearch搜索引擎專用引擎基于 Lucene 引擎編程語言C 語言Java存儲方案內存磁盤協議Redis 序列化協議HTTP集群企業(yè)版支持支持性能簡單查詢高于 ES復雜查詢時高于 RediSearch

RedisJSON 性能數據

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

  • 對于隔離寫入(isolated writes),RedisJSON 比 MongoDB 快 5.4 倍,比 ES 快 200 倍以上

  • 對于隔離讀?。╥solated reads),RedisJSON 比 MongoDB 快 12.7 倍,比 ES 快 500 倍以上

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

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

  • RedisJSON 的延遲比 MongoDB 低約 90 倍,比 ES 低 23.7 倍。

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

總結

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

  1. 多線程 + CK 翻頁方案,最壞耗時為 10s~18s

  2. 單線程 + ES?scroll scan?深翻頁方案,相比 CK 方案,并未見到明顯優(yōu)化

  3. ES + Hbase 組合方案,最壞耗時優(yōu)化到了 3s~6s

  4. RediSearch + RedisJSON 組合方案,后續(xù)會實測該方案的耗時


如何在千萬級數據中查詢 10W 的數據,都有什么方案?的評論 (共 條)

分享到微博請遵守國家法律
竹山县| 哈尔滨市| 红河县| 重庆市| 三亚市| 怀仁县| 华坪县| 锡林郭勒盟| 西藏| 饶阳县| 梁山县| 文安县| 仙居县| 万源市| 阳曲县| 中阳县| 大化| 景德镇市| 河东区| 涟源市| 青阳县| 石嘴山市| 渭源县| 盐源县| 五台县| 南木林县| 石家庄市| 天津市| 泰来县| 温州市| 崇义县| 桃源县| 项城市| 阿合奇县| 定西市| 呈贡县| 肃宁县| 鹿邑县| 鄯善县| 临海市| 武胜县|