关于java:面试官如何在千万级数据中查询-10W-的数据都有什么方案

作者:变速风声 \
链接: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 组合计划,后续会实测该计划的耗时

参考资料:

  • https://juejin.cn/post/7103848212154286087
  • https://www.infoq.cn/article/wymrl5h80sfawg8u7ede
  • https://juejin.cn/post/7042476201574662175

近期热文举荐:

1.1,000+ 道 Java面试题及答案整顿(2022最新版)

2.劲爆!Java 协程要来了。。。

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

4.别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞+转发哦!

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据