面试题:千万量级数据中查询 10W 量级的数据有什么方案?

2023-12-03 16:04

本文主要是介绍面试题:千万量级数据中查询 10W 量级的数据有什么方案?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 初版设计方案
    • 整体方案设计为:
    • 技术方案如下:
    • CK 分页查询
    • 使用 ES Scroll Scan 优化深翻页
      • 耗时数据
    • ES+Hbase 组合查询方案
      • ES 查询的两个阶段
      • 组合使用 Hbase
    • RediSearch+RedisJSON 优化方案
      • RediSearch 性能数据
      • RedisJSON 性能数据
  • 总结


前言

在开发中遇到一个业务诉求,需要在千万量级的底池数据中筛选出不超过 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;    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 进行排序,得到最终的「结果数据」。

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 翻页,有下面几种方案:

  • from + size 翻页
  • scroll 翻页
  • scroll scan 翻页
  • search after 翻页
    图片

对上述几种翻页方案,查询不同数目的数据,耗时数据如下表:

图片

耗时数据

此处,分别使用 Elasticsearch 的 scroll scan 翻页方案、初版中的 CK 翻页方案进行数据查询,对比其耗时数据。

图片

图片

如上测试数据,可以发现,以十万,百万,千万量级的底池为例:

  • 底池量级越大,查询相同的数据量,耗时越大
  • 查询结果 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。

图片

从中可以得出如下结论:

  • 一次 ES 查询中,若查询字段和信息较多,fetch 阶段的耗时,远大于 query 阶段的耗时。
  • 一次 ES 查询中,若查询字段和信息较多,通过减少不必要的查询字段,可以显著缩短查询耗时。
    下面对结论中涉及的 query 和 fetch 查询阶段进行补充说明。

ES 查询的两个阶段

在 ES 中,搜索一般包括两个阶段:

  • query 阶段: 根据查询条件,确定要取哪些文档(doc),筛选出文档 ID(doc_id)
  • fetch 阶段: 根据 query 阶段返回的文档 ID(doc_id),取出具体的文档(doc)

组合使用 Hbase

在《ES 亿级数据检索优化,三秒返回突破性能瓶颈》一文调研的基础上,发现「减少不必要的查询展示字段」可以明显缩短查询耗时。

沿着这个优化思路,设计了一种新的查询方案:

  • 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。

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 延迟稍微低些。

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 的数据的场景,不同方案的耗时如下:

  • 多线程+CK 翻页方案,最坏耗时为 10s~18s
  • 单线程+ES scroll scan 深翻页方案,相比 CK 方案,并未见到明显优化
  • ES+Hbase 组合方案,最坏耗时优化到了 3s~6s
  • RediSearch+RedisJSON 组合方案,后续会实测该方案的耗时

这篇关于面试题:千万量级数据中查询 10W 量级的数据有什么方案?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/450021

相关文章

Java中注解与元数据示例详解

《Java中注解与元数据示例详解》Java注解和元数据是编程中重要的概念,用于描述程序元素的属性和用途,:本文主要介绍Java中注解与元数据的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参... 目录一、引言二、元数据的概念2.1 定义2.2 作用三、Java 注解的基础3.1 注解的定义3.2 内

将sqlserver数据迁移到mysql的详细步骤记录

《将sqlserver数据迁移到mysql的详细步骤记录》:本文主要介绍将SQLServer数据迁移到MySQL的步骤,包括导出数据、转换数据格式和导入数据,通过示例和工具说明,帮助大家顺利完成... 目录前言一、导出SQL Server 数据二、转换数据格式为mysql兼容格式三、导入数据到MySQL数据

C++中使用vector存储并遍历数据的基本步骤

《C++中使用vector存储并遍历数据的基本步骤》C++标准模板库(STL)提供了多种容器类型,包括顺序容器、关联容器、无序关联容器和容器适配器,每种容器都有其特定的用途和特性,:本文主要介绍C... 目录(1)容器及简要描述‌php顺序容器‌‌关联容器‌‌无序关联容器‌(基于哈希表):‌容器适配器‌:(

C#提取PDF表单数据的实现流程

《C#提取PDF表单数据的实现流程》PDF表单是一种常见的数据收集工具,广泛应用于调查问卷、业务合同等场景,凭借出色的跨平台兼容性和标准化特点,PDF表单在各行各业中得到了广泛应用,本文将探讨如何使用... 目录引言使用工具C# 提取多个PDF表单域的数据C# 提取特定PDF表单域的数据引言PDF表单是一

MySQL分表自动化创建的实现方案

《MySQL分表自动化创建的实现方案》在数据库应用场景中,随着数据量的不断增长,单表存储数据可能会面临性能瓶颈,例如查询、插入、更新等操作的效率会逐渐降低,分表是一种有效的优化策略,它将数据分散存储在... 目录一、项目目的二、实现过程(一)mysql 事件调度器结合存储过程方式1. 开启事件调度器2. 创

一文详解Python中数据清洗与处理的常用方法

《一文详解Python中数据清洗与处理的常用方法》在数据处理与分析过程中,缺失值、重复值、异常值等问题是常见的挑战,本文总结了多种数据清洗与处理方法,文中的示例代码简洁易懂,有需要的小伙伴可以参考下... 目录缺失值处理重复值处理异常值处理数据类型转换文本清洗数据分组统计数据分箱数据标准化在数据处理与分析过

大数据小内存排序问题如何巧妙解决

《大数据小内存排序问题如何巧妙解决》文章介绍了大数据小内存排序的三种方法:数据库排序、分治法和位图法,数据库排序简单但速度慢,对设备要求高;分治法高效但实现复杂;位图法可读性差,但存储空间受限... 目录三种方法:方法概要数据库排序(http://www.chinasem.cn对数据库设备要求较高)分治法(常

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

数据库oracle用户密码过期查询及解决方案

《数据库oracle用户密码过期查询及解决方案》:本文主要介绍如何处理ORACLE数据库用户密码过期和修改密码期限的问题,包括创建用户、赋予权限、修改密码、解锁用户和设置密码期限,文中通过代码介绍... 目录前言一、创建用户、赋予权限、修改密码、解锁用户和设置期限二、查询用户密码期限和过期后的修改1.查询用

Python将大量遥感数据的值缩放指定倍数的方法(推荐)

《Python将大量遥感数据的值缩放指定倍数的方法(推荐)》本文介绍基于Python中的gdal模块,批量读取大量多波段遥感影像文件,分别对各波段数据加以数值处理,并将所得处理后数据保存为新的遥感影像... 本文介绍基于python中的gdal模块,批量读取大量多波段遥感影像文件,分别对各波段数据加以数值处