在开发中遇到一个业务诉求,需要在千万量级的底池数据中筛选出不超过 10W 的数据,并根据配置的权重规则进行排序、打散(如同一个类目下的商品数据不能连续出现 3 次)。下面对该业务诉求的实现,设计思路和方案优化进行介绍。
对“千万量级数据中查询 10W 量级的数据”设计了如下方案:
-
-
-
-
RediSearch+RedisJSON 组合方案
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
-
视频教程:https://doc.iocoder.cn/video/
整体方案设计为:
-
先根据配置的「筛选规则」,从底池表中筛选出「目标数据」
-
在根据配置的「排序规则」,对「目标数据」进行排序,得到「结果数据」
技术方案如下:
①
每天运行导数任务,把现有的千万量级的底池数据(Hive 表)导入到 Clickhouse 中,后续使用 CK 表进行数据筛选。
②
将业务配置的筛选规则和排序规则,构建为一个「筛选 + 排序」对象 SelectionQueryCondition。
③
从 CK 底池表取「目标数据」时,开启多线程,进行分页筛选,将获取到的「目标数据」存放到 result 列表中。
//分页大小 默认 5000
int pageSize = this.getPageSize();
//页码数
int pageCnt = totalNum / this.getPageSize() + 1;
List
④对目标数据 result 进行排序,得到最终的「结果数据」。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/yudao-cloud
-
视频教程:https://doc.iocoder.cn/video/
在「初版设计方案」章节的第 3 步提到了「从 CK 底池表取目标数据时,开启多线程,进行分页筛选」。此处对 CK 分页查询进行介绍。
①封装了 queryPoolSkuList 方法,负责从 CK 表中获得目标数据。该方法内部调用了 sqlSession.selectList 方法。
public List> queryPoolSkuList( Map params ) {
List> resultMaps = new ArrayList<>();
QueryCondition queryCondition = parseQueryCondition(params);
List> mapList = lianNuDao.queryPoolSkuList(getCkDt(),queryCondition);
if (CollectionUtils.isNotEmpty(mapList)) {
for (Map data : mapList) {
resultMaps.add(camelKey(data));
}
}
return resultMaps;
}
// lianNuDao.queryPoolSkuList
@Autowired
@Qualifier("ckSqlNewSession")
private SqlSession sqlSession;
public List> queryPoolSkuList( String dt, QueryCondition queryCondition ) {
queryCondition.setDt(dt);
queryCondition.checkMultiQueryItems();
return sqlSession.selectList("LianNu.queryPoolSkuList",queryCondition);
}
②sqlSession.selectList 方法中调用了和 CK 交互的 queryPoolSkuList 查询方法,部分代码如下:
③可以看到,在 CK 分页查询时,是通过 limit #{limitStart},#{limitEnd} 实现的分页。
limit 分页方案,在「深翻页」时会存在性能问题。初版方案上线后,在 1000W 量级的底池数据中筛选 10W 的数据,最坏耗时会达到 10s~18s 左右。
对于 CK 深翻页时候的性能问题,进行了优化,使用 Elasticsearch 的 scroll scan 翻页方案进行优化。
ES 翻页,有下面几种方案:
对上述几种翻页方案,查询不同数目的数据,耗时数据如下表:
此处,分别使用 Elasticsearch 的 scroll scan 翻页方案、初版中的 CK 翻页方案进行数据查询,对比其耗时数据。
如上测试数据,可以发现,以十万,百万,千万量级的底池为例:
-
-
查询结果 3W 以下时,ES 性能优;查询结果 5W 以上时,CK 多线程性能优
在「使用 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 中,搜索一般包括两个阶段:
-
query 阶段:
根据查询条件,确定要取哪些文档(doc),筛选出文档 ID(doc_id)
-
fetch 阶段:
根据 query 阶段返回的文档 ID(doc_id),取出具体的文档(doc)
在《ES 亿级数据检索优化,三秒返回突破性能瓶颈》一文调研的基础上,发现「减少不必要的查询展示字段」可以明显缩短查询耗时。
沿着这个优化思路,设计了一种新的查询方案:
-
ES 仅用于条件筛选,ES 的查询结果仅包含记录的唯一标识 sku_id(其实还包含 ES 为每条文档记录的 doc_id)