当前位置: 首页 > 科技观察

千万级数据查询:CK、ES、RediSearch谁是王者?

时间:2023-03-14 20:34:49 科技观察

开发时遇到一个业务需求,需要从几千万锅数据中筛选出不超过10W的数据,按照配置的权重规则进行排序和分散(如同一商品下category数据不能连续出现3次)。下面介绍业务需求的实现、设计思路和方案优化。针对“千万级数据中查询10W级数据”设计了以下方案:多线程+CK翻页方案ES滚动扫描深度翻页方案ES+Hbase组合方案RediSearch+RedisJSON组合方案初步设计方案总体方案设计为:首先根据配置的过滤规则从pot表中过滤出目标数据,然后根据配置的排序规则对目标数据进行排序,得到结果数据。量级的锅数据(Hive表)导入Clickhouse,然后使用CK表进行数据筛选。②将业务配置的过滤规则和排序规则构造成一个筛选+排序对象SelectionQueryCondition。③从CKpot表中取目标数据时,启动多线程,进行分页筛选,将获取到的目标数据存入结果列表。//页面大小默认5000intpageSize=this.getPageSize();//页码intpageCnt=totalNum/this.getPageSize()+1;List>result=Lists.newArrayList();List>>>futureList=newArrayList<>(pageCnt);//开启多线程调用for(inti=1;i<=pageCnt;i++){//配置业务筛选规则和顺序规则构造为SelectionQueryCondition对象SelectionQueryConditionselectionQueryCondition=buildSelectionQueryCondition(selectionQueryRuleData);selectionQueryCondition.setPageSize(pageSize);selectionQueryCondition.setPage(i);futureList.add(selectionQueryEventPool.submit(newQuerySelectionDataThread(selectionQueryCondition)));}for(Future>>future:futureList){//RPC调用List>queryRes=future.get(20,TimeUnit.SECONDS);if(CollectionUtils.isNotEmpty(queryRes)){//将目标数据存入resultresult.addAll(query资源);}}④对目标数据result进行排序,得到最终结果数据CK分页查询第一版设计方案章节第三步中提到,从CKpot表中取出目标数据时,多线程是启用执行寻呼筛选。这里介绍CK分页查询。①封装queryPoolSkuList方法,负责从CK表中获取目标数据。此方法在内部调用sqlSession.selectList方法。publicList>queryPoolSkuList(Mapparams){List>resultMaps=newArrayList<>();QueryConditionqueryCondition=parseQueryCondition(params);List>mapList=lianNuDao.queryPoolSkuList(getCkDt(),queryCondition);如果(CollectionUtils.isNotEmpty(mapList)){for(Mapdata:mapList){resultMaps.add(camelKey(data));}}returnresultMaps;}//lianNuDao.queryPoolSkuList@Autowired@Qualifier("ckSqlNewSession")privateSqlSessionsqlSession;publicList>queryPoolSkuList(Stringdt,QueryConditionqueryCondition){queryCondition.setDt(dt);queryCondition.checkMultiQueryItems();returnsqlSession.selectList("LianNu.queryPoolSkuList",queryCondition);}②sqlSession.selectList方法中调用了和CK交互的queryPoolSkuList查询方法,部分代码如下:selectsku_pool_id,item_sku_id,skuPoolName,price,......businessTypefromliannu_sku_pool_indicator_allwheredt=#{dt}和${queryItem.field}=#{queryItem.value}......按sku_pool_id,item_sku_id分组groupbysku_pool_id,item_sku_id,${orderBy}orderby${orderBy}${orderAd}limit#{limitStart},#{limitEnd}③可以看到,在CK分页查询时,是通过limit#{limitStart},#{limitEnd}实现了paginglimit分页方案,深度翻页时会有性能问题。第一版方案上线后,从1000W锅数据中过滤出10W数据最差也需要10s到18s左右。使用ESScrollScan优化深度翻页优化CK深度翻页的性能问题,使用Elasticsearch的scrollscan翻页方案进行优化。ES的翻页方案ES翻页有以下几种方案:from+size翻页scroll翻页scrollscan翻页翻页后搜索对于上述翻页方案,查询不同数量的数据,耗时数据如下表所示:耗时数据这里使用Elasticsearch的滚动扫描翻页方案和第一版中的CK翻页方案进行数据查询,对比它们的耗时数据.从上面的测试数据可以发现,以十万、百万、千万的锅为例:锅的量级越大,相同数据量的查询时间越长。当查询结果小于3W时,ES性能优秀。;当查询结果大于5W时,CK多线程性能优秀。ES+Hbase组合查询方案使用ESScrollScan优化深度翻页,使用Elasticsearch的滚动扫描翻页方案优化深度翻页问题,但是实现时是单线程调用,所以耗时数据最终测试不是特别理想,性能和CK翻页方案差不多。研究阶段发现,当10W的目标数据从锅中取出时,一个产品包含多个字段的信息(CK表中一行有150个字段信息),如价格、会员价、学生价格、库存、优惠率等。对于一行记录,当获取的字段数量减少时,查询耗时会明显减少。例如sku1的商品,从之前获取价格、会员价、学生价、亲友价、库存等100个字段信息,缩减为只有价格和库存两??个字段信息。如下图,使用ES查询方案,对于查询相同条数记录的场景(从千万盆中筛选出70000+条数据),每条记录获取的字段数减少从32个减到17个,再减到1个(其实是两个字段,一个是商品的唯一标识sku_id,一个是ES为每个文档记录的doc_id),查询时间会从9.3s下降到4.2s,然后到2.4s。由此,我们可以得出以下结论:在ES查询中,如果查询字段和信息较多,fetch阶段的耗时要比query阶段长很多。在ES查询中,如果查询字段和信息较多,通过减少不必要的查询字段,可以显着缩短查询时间。下面对结论中涉及的query和fetch查询阶段进行补充说明。ES查询的两个阶段在ES中,搜索一般包括两个阶段:query阶段:根据查询条件确定取哪些文档(doc),并过滤出文档ID(doc_id)fetch阶段:根据返回的文档ID通过查询阶段(doc_id),取出具体的文档(doc),使用Hbase减少不必要的查询显示字段,可以显着缩短查询时间。遵循这种优化思路,设计了一种新的查询方案:ES只用于条件过滤,ES的查询结果只包含记录的唯一标识sku_id(实际上还包含ES记录的每个文档的doc_id).Hbase是一种列式存储的数据库,每一列数据都有一个rowKey。使用rowKey过滤一条记录时,复杂度为O(1)。(类似于根据key从HashMap取值)根据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.5Kops/sec,ES的吞吐量达到了3.1Kops/sec。RediSearch比ES快4倍。同时,RediSearch的延迟是8ms,而ES是10ms,RediSearch的延迟稍低。RedisJSON性能数据根据官网的性能测试报告,RedisJson+RedisSearch可谓碾压其他NoSQL:对于孤立写,RedisJSON比MongoDB快5.4倍,比ES快200多倍。对于隔离读取,RedisJSON比MongoDB快12.7倍,比ES快500多倍在混合工作负载场景下,实时更新不会影响RedisJSON的搜索和读取性能,而ES会:RedisJSON支持约10000操作/秒,高于MongoDB比ES高50倍,高7倍RedisJSON的延迟比MongoDB低90倍,比ES低23.7倍此外,RedisJSON的读取、写入和加载搜索延迟在更高的百分位比ES高得多,MongoDB很稳定。RedisJSON也可以在提高写入率时处理越来越高的整体吞吐量。并且当写入比例增加时,ES会降低它可以处理的整体吞吐量。总结本文由一个业务需求触发,介绍了“从千万级数据中查询10W级数据”的不同设计方案。1000W锅数据筛选10W数据场景,不同方案耗时如下:多线程+CK翻页方案,最差耗时10s~18s单线程+ES滚动扫描深页-turning方案,对比CK方案,ES+Hbase组合方案没有明显优化,耗时优化最差的是3s~6sRediSearch+RedisJSON组合方案,实测该方案耗时之后