企业建站系统开源,普通网站建设,山西省住房和城乡建设厅网站报名,建设团购网站费用前言
在开发中遇到一个业务诉求#xff0c;需要在千万量级的底池数据中筛选出不超过 10W 的数据#xff0c;并根据配置的权重规则进行排序、打散#xff08;如同一个类目下的商品数据不能连续出现 3 次#xff09;。
下面对该业务诉求的实现#xff0c;设计思路和方案优…前言
在开发中遇到一个业务诉求需要在千万量级的底池数据中筛选出不超过 10W 的数据并根据配置的权重规则进行排序、打散如同一个类目下的商品数据不能连续出现 3 次。
下面对该业务诉求的实现设计思路和方案优化进行介绍对「千万量级数据中查询 10W 量级的数据」设计了如下方案 多线程 CK 翻页方案 ES scroll scan 深翻页方案 ES Hbase 组合方案 RediSearch RedisJSON 组合方案
初版设计方案
整体方案设计为 先根据配置的「筛选规则」从底池表中筛选出「目标数据」 在根据配置的「排序规则」对「目标数据」进行排序得到「结果数据」
技术方案如下 每天运行导数任务把现有的千万量级的底池数据Hive 表导入到 Clickhouse 中后续使用 CK 表进行数据筛选。 将业务配置的筛选规则和排序规则构建为一个「筛选 排序」对象 SelectionQueryCondition。 从 CK 底池表取「目标数据」时开启多线程进行分页筛选将获取到的「目标数据」存放到 result 列表中。
//分页大小 默认 5000
int pageSize this.getPageSize();
//页码数
int pageCnt totalNum / this.getPageSize() 1;ListMapString, Object result Lists.newArrayList();
ListFutureListMapString, 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 (FutureListMapString, Object future : futureList) {//RPC 调用ListMapString, Object queryRes future.get(20, TimeUnit.SECONDS);if (CollectionUtils.isNotEmpty(queryRes)) {// 将目标数据存放在 result 中result.addAll(queryRes);}
} 对目标数据 result 进行排序得到最终的「结果数据」。
CK分页查询
在「初版设计方案」章节的第 3 步提到了「从 CK 底池表取目标数据时开启多线程进行分页筛选」。此处对 CK 分页查询进行介绍。
封装了 queryPoolSkuList 方法负责从 CK 表中获得目标数据。该方法内部调用了 sqlSession.selectList 方法。
public ListMapString, Object queryPoolSkuList( MapString, Object params ) {ListMapString, Object resultMaps new ArrayList();QueryCondition queryCondition parseQueryCondition(params);ListMapString, Object mapList lianNuDao.queryPoolSkuList(getCkDt(),queryCondition);if (CollectionUtils.isNotEmpty(mapList)) {for (MapString,Object data : mapList) {resultMaps.add(camelKey(data));}}return resultMaps;
}// lianNuDao.queryPoolSkuListAutowired
Qualifier(ckSqlNewSession)
private SqlSession sqlSession;public ListMapString, Object queryPoolSkuList( String dt, QueryCondition queryCondition ) {queryCondition.setDt(dt);queryCondition.checkMultiQueryItems();return sqlSession.selectList(LianNu.queryPoolSkuList,queryCondition);
}
sqlSession.selectList 方法中调用了和 CK 交互的 queryPoolSkuList 查询方法部分代码如下。
select idqueryPoolSkuList parameterTypecom.jd.bigai.domain.liannu.QueryCondition resultTypejava.util.Mapselect sku_pool_id,item_sku_id,skuPoolName,price,......businessTypefrom liannu_sku_pool_indicator_allwheredt#{dt}andforeach collectionqueryItems separator and itemqueryItem open close choosewhen testqueryItem.type equal${queryItem.field} #{queryItem.value}/when....../choose/foreachif testorderBy nullgroup by sku_pool_id,item_sku_id/ifif testorderBy ! nullgroup by sku_pool_id,item_sku_id,${orderBy} order by ${orderBy} ${orderAd}/ifif testlimitEnd ! 0limit #{limitStart},#{limitEnd}/if
/select
可以看到在 CK 分页查询时是通过 limit #{limitStart},#{limitEnd} 实现的分页。
limit 分页方案在「深翻页」时会存在性能问题。初版方案上线后在 1000W 量级的底池数据中筛选 10W 的数据最坏耗时会达到 10s~18s 左右。
使用ES Scroll Scan 优化深翻页
对于 CK 深翻页时候的性能问题进行了优化使用 Elasticsearch 的 scroll scan 翻页方案进行优化。
另外ES 面试题整理发了如果你近期准备面试跳槽建议在Java面试库小程序在线刷题涵盖 2000 道 Java 面试题几乎覆盖了所有主流技术面试题。
ES的翻页方案
ES 翻页有下面几种方案 from size 翻页 scroll 翻页 scroll scan 翻页 search after 翻页
翻页方式性能优点缺点场景from size低灵活性好实现简单深度分页问题数据量比较小能容忍深度分页问题scroll中解决了深度分页问题需要维护一个 scrollId快照版本无法反应数据的实时性可排序但无法跳页查询查询海量数据scroll scan中基于 scroll 方案进一步提升了海量数据查询的性能无法排序其余缺点同 scroll查询海量数据search after高性能最好不存在深度分页问题能够反映数据的实时变更实现复杂需要有一个全局唯一的字段。连续分页的实现会比较复杂因为每一次查询都需要上次查询的结果不适用于大幅度跳页查询适用于海量数据的分页
对上述几种翻页方案查询不同数目的数据耗时数据如下表。
ES 翻页方式1-1049000-4901099000-99010from size8ms30ms117msscroll7ms66ms36mssearch_after5ms8ms7ms耗时数据
此处分别使用 Elasticsearch 的 scroll scan 翻页方案、初版中的 CK 翻页方案进行数据查询对比其耗时数据。 如上测试数据可以发现以十万百万千万量级的底池为例 底池量级越大查询相同的数据量耗时越大 查询结果 3W 以下时ES 性能优查询结果 5W 以上时CK 多线程性能优
ESHbase组合查询方案
在「使用 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。 从中可以得出如下结论 一次 ES 查询中若查询字段和信息较多fetch 阶段的耗时远大于 query 阶段的耗时。 一次 ES 查询中若查询字段和信息较多通过减少不必要的查询字段可以显著缩短查询耗时。
下面对结论中涉及的 query 和 fetch 查询阶段进行补充说明。
另外ES 面试题整理发了如果你近期准备面试跳槽建议在Java面试库小程序在线刷题涵盖 2000 道 Java 面试题几乎覆盖了所有主流技术面试题。
在 ES 中搜索一般包括两个阶段query 和 fetch 阶段
query 阶段 根据查询条件确定要取哪些文档doc筛选出文档 IDdoc_id
fetch 阶段 根据 query 阶段返回的文档 IDdoc_id取出具体的文档doc
ES的filesystem cache ES 会将磁盘中的数据自动缓存到 filesystem cache在内存中查找提升了速度 若 filesystem cache 无法容纳索引数据文件则会基于磁盘查找此时查询速度会明显变慢 若数量两过大基于「ES 查询的的 query 和 fetch 两个阶段」可使用 ES HBase 架构保证 ES 的数据量小于 filesystem cache保证查询速度
组合使用Hbase
在上文调研的基础上发现「减少不必要的查询展示字段」可以明显缩短查询耗时。沿着这个优化思路参照参考链接 ref-1设计了一种新的查询方案 ES 仅用于条件筛选ES 的查询结果仅包含记录的唯一标识 sku_id其实还包含 ES 为每条文档记录的 doc_id Hbase 是列存储数据库每列数据有一个 rowKey。利用 rowKey 筛选一条记录时复杂度为 O(1)。类似于从 HashMap 中根据 key 取 value 根据 ES 查询返回的唯一标识 sku_id作为 Hbase 查询中的 rowKey在 O(1) 复杂度下获取其他信息字段如价格库存等。 使用 ES Hbase 组合查询方案在线上进行了小规模的灰度测试。在 1000W 量级的底池数据中筛选 10W 的数据对比 CK 翻页方案最坏耗时从 10~18s 优化到了 3~6s 左右。
也应该看到使用 ES Hbase 组合查询方案会增加系统复杂度同时数据也需要同时存储到 ES 和 Hbase。
RediSearchRedisJSON优化方案
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/secES 的吞吐量为 3.1K ops/secRediSearch 比ES 要快 4 倍。同时RediSearch 的延迟为 8ms而 ES 为 10msRediSearch 延迟稍微低些。
对比RedisearchElasticsearch搜索引擎专用引擎基于 Lucene 引擎编程语言C 语言Java存储方案内存磁盘协议Redis 序列化协议HTTP集群企业版支持支持性能简单查询高于 ES复杂查询时高于 RediSearch
RedisJSON 性能数据
根据官网的性能测试报告RedisJson RedisSearch 可谓碾压其他 NoSQL 对于隔离写入isolated writesRedisJSON 比 MongoDB 快 5.4 倍比 ES 快 200 倍以上 对于隔离读取isolated readsRedisJSON 比 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 的数据」的场景不同方案的耗时如下 多线程 CK 翻页方案最坏耗时为 10s~18s 单线程 ES scroll scan 深翻页方案相比 CK 方案并未见到明显优化 ES Hbase 组合方案最坏耗时优化到了 3s~6s RediSearch RedisJSON 组合方案后续会实测该方案的耗时