zoukankan      html  css  js  c++  java
  • 华为云·寻找黑马程序员#海量数据的分页怎么破?【华为云技术分享】

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/devcloud/article/details/96869376

    欢迎添加华为云小助手微信(微信号:HWCloud002HWCloud003),验证通过后,输入关键字“加群”,加入华为云线上技术讨论群;输入关键字“最新活动”,获取华为云最新特惠促销。华为云诸多技术大咖、特惠活动等你来撩!

    一、背景

    分页应该是极为常见的数据展现方式了,一般在数据集较大而无法在单个页面中呈现时会采用分页的方法。
    各种前端UI组件在实现上也都会支持分页的功能,而数据交互呈现所相应的后端系统、数据库都对数据查询的分页提供了良好的支持。
    以几个流行的数据库为例:

    查询表 t_data 第 2 页的数据(假定每页 5 条)

    • MySQL 的做法:
    select * from t_data limit 5,5
    • PostGreSQL 的做法:
    select * from t_data limit 5 offset 5
    • MongoDB 的做法:
    db.t_data.find().limit(5).skip(5);

    尽管每种数据库的语法不尽相同,通过一些开发框架封装的接口,我们可以不需要熟悉这些差异。如 SpringData 提供的分页接口:

    1 public interface PagingAndSortingRepository
    2   extends CrudRepository {
    3 
    4   Page findAll(Pageable pageable);
    5 }

    这样看来,开发一个分页的查询功能是非常简单的。
    然而万事皆不可能尽全尽美,尽管上述的数据库、开发框架提供了基础的分页能力,在面对日益增长的海量数据时却难以应对,一个明显的问题就是查询性能低下!
    那么,面对千万级、亿级甚至更多的数据集时,分页功能该怎么实现?

    下面,我以 MongoDB 作为背景来探讨几种不同的做法。

    二、传统方案

    就是最常规的方案,假设 我们需要对文章 articles 这个表(集合) 进行分页展示,一般前端会需要传递两个参数:
    - 页码(当前是第几页)
    - 页大小(每页展示的数据个数)

    按照这个做法的查询方式,如下图所示:

    因为是希望最后创建的文章显示在前面,这里使用了_id 做降序排序
    其中红色部分语句的执行计划如下:

     1 {
     2   "queryPlanner" : {
     3     "plannerVersion" : 1,
     4     "namespace" : "appdb.articles",
     5     "indexFilterSet" : false,
     6     "parsedQuery" : {
     7       "$and" : []
     8     },
     9     "winningPlan" : {
    10       "stage" : "SKIP",
    11       "skipAmount" : 19960,
    12       "inputStage" : {
    13         "stage" : "FETCH",
    14         "inputStage" : {
    15           "stage" : "IXSCAN",
    16           "keyPattern" : {
    17             "_id" : 1
    18           },
    19           "indexName" : "_id_",
    20           "isMultiKey" : false,
    21           "direction" : "backward",
    22           "indexBounds" : {
    23             "_id" : [ 
    24               "[MaxKey, MinKey]"
    25             ]
    26          ...
    27 }

    可以看到随着页码的增大,skip 跳过的条目也会随之变大,而这个操作是通过 cursor 的迭代器来实现的,对于cpu的消耗会比较明显。
    而当需要查询的数据达到千万级及以上时,会发现响应时间非常的长,可能会让你几乎无法接受!

    或许,假如你的机器性能很差,在数十万、百万数据量时已经会出现瓶颈

    三、改良做法

    既然传统的分页方案会产生 skip 大量数据的问题,那么能否避免呢?答案是可以的。
    改良的做法为:
    1. 选取一个唯一有序的关键字段,比如 _id,作为翻页的排序字段;
    2. 每次翻页时以当前页的最后一条数据_id值作为起点,将此并入查询条件中。

    如下图所示:

    修改后的语句执行计划如下:

     1 {
     2   "queryPlanner" : {
     3     "plannerVersion" : 1,
     4     "namespace" : "appdb.articles",
     5     "indexFilterSet" : false,
     6     "parsedQuery" : {
     7       "_id" : {
     8         "$lt" : ObjectId("5c38291bd4c0c68658ba98c7")
     9       }
    10     },
    11     "winningPlan" : {
    12       "stage" : "FETCH",
    13       "inputStage" : {
    14         "stage" : "IXSCAN",
    15         "keyPattern" : {
    16           "_id" : 1
    17         },
    18         "indexName" : "_id_",
    19         "isMultiKey" : false,
    20         "direction" : "backward",
    21         "indexBounds" : {
    22           "_id" : [ 
    23             "(ObjectId('5c38291bd4c0c68658ba98c7'), ObjectId('000000000000000000000000')]"
    24           ]
    25       ...
    26 }

    可以看到,改良后的查询操作直接避免了昂贵的 skip 阶段,索引命中及扫描范围也是非常合理的!

    性能对比

    为了对比这两种方案的性能差异,下面准备了一组测试数据。

    测试方案
    准备10W条数据,以每页20条的参数从前往后翻页,对比总体翻页的时间消耗

     1 db.articles.remove({});
     2 var count = 100000;
     3 
     4 var items = [];
     5 for(var i=1; i<=count; i++){
     6 
     7   var item = {
     8     "title": "论年轻人思想建设的重要性-" + i,
     9     "author" : "王小兵-" + Math.round(Math.random() * 50),
    10     "type" : "杂文-" + Math.round(Math.random() * 10) ,
    11     "publishDate" : new Date(),
    12   } ;
    13   items.push(item);
    14 
    15 
    16   if(i%1000==0){
    17     db.test.insertMany(items);
    18     print("insert", i);
    19 
    20     items = [];
    21   }
    22 }

    传统翻页脚本

     1 function turnPages(pageSize, pageTotal){
     2 
     3   print("pageSize:", pageSize, "pageTotal", pageTotal)
     4 
     5   var t1 = new Date();
     6   var dl = [];
     7 
     8   var currentPage = 0;
     9   //轮询翻页
    10   while(currentPage &lt; pageTotal){
    11 
    12      var list = db.articles.find({}, {_id:1}).sort({_id: -1}).skip(currentPage*pageSize).limit(pageSize);
    13      dl = list.toArray();
    14 
    15      //没有更多记录
    16      if(dl.length == 0){
    17          break;
    18      }
    19      currentPage ++;
    20      //printjson(dl)
    21   }
    22 
    23   var t2 = new Date();
    24 
    25   var spendSeconds = Number((t2-t1)/1000).toFixed(2)
    26   print("turn pages: ", currentPage, "spend ", spendSeconds, ".")  
    27 
    28 }

    改良翻页脚本

     1 function turnPageById(pageSize, pageTotal){
     2 
     3   print("pageSize:", pageSize, "pageTotal", pageTotal)
     4 
     5   var t1 = new Date();
     6 
     7   var dl = [];
     8   var currentId = 0;
     9   var currentPage = 0;
    10 
    11   while(currentPage ++ &lt; pageTotal){
    12 
    13       //以上一页的ID值作为起始值
    14      var condition = currentId? {_id: {$lt: currentId}}: {};
    15      var list = db.articles.find(condition, {_id:1}).sort({_id: -1}).limit(pageSize);
    16      dl = list.toArray();
    17 
    18      //没有更多记录
    19      if(dl.length == 0){
    20          break;
    21      }
    22 
    23      //记录最后一条数据的ID
    24      currentId = dl[dl.length-1]._id;
    25   }
    26 
    27   var t2 = new Date();
    28 
    29   var spendSeconds = Number((t2-t1)/1000).toFixed(2)
    30   print("turn pages: ", currentPage, "spend ", spendSeconds, ".")    
    31 }

    以100、500、1000、3000页数的样本进行实测,结果如下

    可见,当页数越大(数据量越大)时,改良的翻页效果提升越明显!
    这种分页方案其实采用的就是时间轴(TImeLine)的模式,实际应用场景也非常的广,比如Twitter、微博、朋友圈动态都可采用这样的方式。
    而同时除了上述的数据库之外,HBase、ElasticSearch 在Range Query的实现上也支持这种模式。

    四、完美的分页

    时间轴(TimeLine)的模式通常是做成“加载更多”、上下翻页这样的形式,但无法自由的选择某个页码。
    那么为了实现页码分页,同时也避免传统方案带来的 skip 性能问题,我们可以采取一种折中的方案。

    这里参考Google搜索结果页作为说明:

    通常在数据量非常大的情况下,页码也会有很多,于是可以采用页码分组的方式。
    以一段页码作为一组,每一组内数据的翻页采用ID 偏移量 + 少量的 skip 操作实现

    具体的操作如下图所示:

    实现步骤

    1. 对页码进行分组(groupSize=8, pageSize=20),每组为8个页码;

    2. 提前查询 end_offset,同时获得本组页码数量:

    db.articles.find({ _id: { $lt: start_offset } }).sort({_id: -1}).skip(20*8).limit(1)
    1. 分页数据查询以本页组 start_offset 作为起点,在有限的页码上翻页(skip),由于一个分组的数据量通常很小(8*20=160),在分组内进行skip产生的代价会非常小,因此性能上可以得到保证。

    小结

    随着物联网,大数据业务的白热化,一般企业级系统的数据量也会呈现出快速的增长。而传统的数据库分页方案在海量数据场景下很难满足性能的要求。

    在本文的探讨中,主要为海量数据的分页提供了几种常见的优化方案(以MongoDB作为实例),并在性能上做了一些对比,旨在提供一些参考。

    来源:华为云社区 作者:zale

    欢迎添加华为云小助手微信(微信号:HWCloud002HWCloud003),验证通过后,输入关键字“加群”,加入华为云线上技术讨论群;输入关键字“最新活动”,获取华为云最新特惠促销。华为云诸多技术大咖、特惠活动等你来撩!

    HDC.Cloud 华为开发者大会2020 即将于2020年2月11日-12日在深圳举办,是一线开发者学习实践鲲鹏通用计算、昇腾AI计算、数据库、区块链、云原生、5G等ICT开放能力的最佳舞台。

    欢迎报名参会

  • 相关阅读:
    SpringBoot整合Flyway(数据库版本迁移工具)
    Java并发编程实战 05等待-通知机制和活跃性问题
    Java并发编程实战 04死锁了怎么办?
    Java并发编程实战 03互斥锁 解决原子性问题
    Java并发编程实战 02Java如何解决可见性和有序性问题
    Flutter学习笔记(40)--Timer实现短信验证码获取60s倒计时
    Flutter学习笔记(39)--BottomNavigationBar底部item超过3个只显示icon,不显示title
    Flutter学习笔记(38)--自定义控件之组合控件
    Flutter学习笔记(36)--常用内置动画
    Flutter学习笔记(37)--动画曲线Curves 效果
  • 原文地址:https://www.cnblogs.com/huaweicloud/p/11868431.html
Copyright © 2011-2022 走看看