ElasticSearch分页方案

ElasticSearch分页方案,第1张

"浅"分页是最简单的分页方案。es会根据查询条件在每一个DataNode分片中取出from+size条文档,然后在MasterNode中聚合、排序,再截取size-from的文档返回给调用方。当页数越靠后,也就是from+size越大,es需要读取的数据也就是越大,聚合和排序的时候处理的数据量也越大,此时会加大服务器CPU和内存的消耗。

其中,from定义了目标数据的偏移值,size定义当前返回的数目。默认from为0,size为10,即所有的查询默认仅仅返回前10条数据。

在这里有必要了解一下from/size的原理:

因为es是基于分片的,假设有5个分片,from=100,size=10。则会根据排序规则从5个分片中各取回100条数据数据,然后汇总成500条数据后选择最后面的10条数据。

做过测试,越往后的分页,执行的效率越低。总体上会随着from的增加,消耗时间也会增加。而且数据量越大,就越明显!

from+size查询在10000-50000条数据(1000到5000页)以内的时候还是可以的,但是如果数据过多的话,就会出现深分页问题。

为了解决上面的问题,elasticsearch提出了一个scroll滚动的方式。

scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容,然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景。

scroll=5m表示设置scroll_id保留5分钟可用。

使用scroll必须要将from设置为0。

size决定后面每次调用_search搜索返回的数量

然后我们可以通过数据返回的_scroll_id读取下一页内容,每次请求将会读取下10条数据,直到数据读取完毕或者scroll_id保留时间截止:

注意:请求的接口不再使用索引名了,而是 _search/scroll,其中GET和POST方法都可以使用。

scroll删除

根据官方文档的说法,scroll的搜索上下文会在scroll的保留时间截止后自动清除,但是我们知道scroll是非常消耗资源的,所以一个建议就是当不需要了scroll数据的时候,尽可能快的把scroll_id显式删除掉。

清除指定的scroll_id:

DELETE _search/scroll/DnF1ZXJ5VGhlbkZldGNo

清除所有的scroll:

DELETE _search/scroll/_all

scroll 的方式,官方的建议不用于实时的请求(一般用于数据导出),因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。

search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。

为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。

使用search_after必须要设置from=0。

这里我使用timestamp和_id作为唯一值排序。

我们在返回的最后一条数据里拿到sort属性的值传入到search_after。

使用sort返回的值搜索下一页:

4:修改默认分页限制值10000

可以使用下面的方式来改变ES默认深度分页的indexmax_result_window 最大窗口值

curl -XPUT >

在网站开发、移动 APP 开发的时候,遇到数据量多的时候,都会有人性化的分页功能。但是,看似简单的分页功能,其实存在很多的设计技巧以及不少的坑。

分页有三种样式:普通分页、首末分页、跳转分页


Google的翻页功能的设计

当超过10项的搜索结果,Google会自动分页,你是否曾注意到,这个分页的链接,只出现在网页的底部,而网页顶部却没有分页的链接?分页的链接如下图:

从用户体验的角度,如果顶部也出现分页链接,实际上可有可无。因为按照逻辑的 *** 作,用户起码应该是快速浏览完每页的10项搜索结果,才会浏览下一页,或者干脆按键盘的“End”键,跳到网页底部,按“下一页”。

所以顶部的分页链接作用不大。除非是网页一屏就显示所有的内容,顶部的分页链接才会起到作用,但这时候仍然是可有可无的。


分页设计的两大难点问题

1数据重复

2一次性加载大量信息,加载缓慢


数据重复

传统分页的话,一般只考虑传页数和每页数据条数这两个参数给后端,为了方便后面描述,我们给这个传参方式起个名字叫传统分页。

这种传参方式对于静态数据(数据不会变动)的分页是没问题的,因为每条数据的顺序、数据的总量,都是不变的。

如果出现数据顺序变动或者数据总量变动的分页需求时,单纯的传page和limit已经不能解决了。不同的需求需要显示的列表也不一样。

关于列表分页主要关系到两个方面:

Ø总量(列表头插入了新数据)

Ø排列顺序

传统分页在总量不变,排列顺序不变的列表下是没有任何问题的,但只要这两个要素其中一个是变化的。

例如:

总量不变,排列顺序改变:排行榜

总量改变,排列顺序不变:文章留言列表

总量改变,排列顺序改变:评论列表(点赞数倒叙)

排行榜

现在有一个积分排行榜

假定每页显示3条数据,在某一时刻拿第一页数据时,得到 A、B、C三条数据。就在此时,用户D突然增加了100积分,最新的排行榜情况变成了

传统分页情况下,获取第二页数据时,从当前排行榜第四条数据开始获取,得到 C、E,用户看到的数据就变成 A、B、C、C、E。

C出现了2次,而且D消失了。这就是传统分页用在数据排列顺序会改变的列表时会出现的问题,因为列表顺序改变导致出现重复数据和丢失数据。

这种总量不变,排列顺序改变的分页问题暂时有两种方案解决:

一次性取出、

排行榜快照、通过变动记录表拿数据。


一次性取出(针对特殊需求)


这里说的一次性取出是针对类似“top100”这种取有限条数的需求。

在比较简单的列表数据结构下一次性取出100条数据对服务器性能来说问题不大,但是在复杂数据结构下(涉及关联多个表、数据格式化、数据处理等)一次性处理100或更多的数据是糟糕的做法。

排行榜主要的分页问题是影响排名的字段的值在不断变化导致列表顺序不断改变,我们现在可以一次性取出整个列表但是又担心复杂的数据结构导致服务器性能问题。

如果把整个功能拆分一下,用异步的思想来做这个功能设计如何呢?

分两个接口来做这个功能:获取排行榜列表和获取用户排行榜数据。

获取排行榜列表接口一次性取整个排名列表的用户ID和排名相关的字段数据,这样就保证了整个列表的排序是不变的同时,又不增大服务器性能。

获取用户排行榜数据接口 负责取排行榜要显示的用户的其他数据,这个接口接受多个用户ID的作为参数。

这个接口做了类似分页的功能,前端每次从排行榜中按分页的方式按顺序取部分用户ID,然后通过这个接口获取具体数据显示给用户。

下面以例子的方式来做具体说明:

这是一个积分排行 top100

这里的排行条件是 积分,那我们的 获取排行榜列表接口 只需要取“用户ID”和“积分”即可,剩下的 “昵称”、“胜率”等数据通过 获取用户排行榜数据接口获取。

前端先请求列表接口,获取到一下数据:

然后根据这个列表数据,先取前10条的用户ID:5、12、60、2、77… 请求获取用户排行榜数据接口,把获得的用户数据填充到排行榜中。

当用户下滑加载更多数据时再去列表取在11-20的用户ID重复上面的 *** 作。

如果是 top100 的需求,这个方案是比较推荐的,因为没有性能和储存空间上的额外消耗。


排行榜快照(推荐)

因为考虑到主要问题出在排列顺序是变化的,而且通过其他APP也有看到过按时刷新的排行榜,所以想到了用快照的方式来解决。

可以通过写一个定时脚本,每5分钟生成一次排行榜的快照信息并存下来。接口请求时直接从快照中取数据,这一定程度上解决了列表排序一直在变化问题。

这里之所以说只解决了一定程度,是因为在每次刷新快照数据的时候,可能有用户刚好卡在这个时间点之间去请求(刷新快照前用户请求了第一页数据,刷新快照后用户请求第二页,这就出现传统分页同样的问题了)。

可以通过在快照中加上版本号来解决问题。

例如在生成快照的时候以当前时间戳作为版本号跟快照数据一起保存,同时需要系统保存多份快照数据以便用户获取旧快照数据。请求接口时默认拿最新版本的快照,如果接口传入了版本号就拿对应版本号的快照数据。

优点:

通俗易懂,传参方式跟传统分页类似。

请求处理效率高,生成快照时可以把数据进行处理再保存(例如日期格式转换、类型key值转类型名字等),使得请求到来时获取的数据可以直接返回给用户,无需再做处理。

易于测试和排查,在生成快照那一刻已经决定了整个列表的数据展示,测试和错误排查很方便。

缺点:

实时性比较差,用户拿到的数据不是最新的。

需要额外存储空间,需要额外的地方存储多个版本的快照数据。

需要定时器,对于本来存在定时器的系统架构,这一点不算缺点。

通过变动记录表拿数据

每个完备的系统都会有数据变动的记录表,用于追踪数据变动和 *** 作明细。记录变记录着数据每次变动前后的变化和变动时间,这一特性为使得数据的每次变动都有迹可循,我们就是利用这一点来做排行榜的分页。

我们分页出问题的地方就是因为数据在不断变化导致排序不停改变。

上面说到每次数据变动都会有记录,那我们只需要根据某一时刻之前用户的数据来做排名,是不是就解决数据不断变动这个问题。

文字表达可能不太直观,看下面的数据演示应该能比较好理解。假定用户 A、B、C 初始默认都是100积分

表:score_log

表格中为了方便查看,用了varchar类型表示时间,在实际应用中应该使用int型来存储,因为需要加索引。

假定在03分的时候请求了数据,通过SQL语句就可以拿到03分之前的数据排行。

得到第一页数据:

第二页数据:

关于这种方式的请求,前端需要记录发起第一次请求时的时间,以后每页的请求都带着这个时间。

优点:

Ø无需额外存储数据,利用系统原有数据结构来解决数据变动问题,也无需做多版本控制。

Ø数据相对实时,每次拿到的排行榜数据都是请求第一页那一刻最新的数据。

缺点:

Ø效率相对较差,由于数据需要实时排序和获取,效率相比排行榜要低。而且上面例子只取了记录表中最基础的数据,实际需求中一般需要关联更多的表去取信息,所以效率将随着需求负责度增大而降低。

Ø只适用于用户量不大的情况,由于数据变动记录表的数据量随着用户量的递增是呈倍数递增的,所以用户量达到一定程度的情况下,这个方式效率会变得相当低。


文章评论列表

评论列表一般按照倒叙排列,而且顺序不变。因为是倒叙排列,所以最新的用户评论会放在最顶部,这就会导致问题了。我们还是用实际例子来说。

假定每页拿3条数据,此时请求第一页,得到ID分别5、4、3的评论。在请求第二页之前,突然又来了一条留言,此时列表变成:

用传统分页方式,此时获取第二页会得到ID 3、2、1,这里ID 3 就重复取出来了。

这个问题的解决方案相比排行榜列表分页问题简单而且易懂。评论ID是一个自增的字段,新的评论ID总是比旧评论ID要大,利用这一点我们可以很好的解决问题。

接口传参:

说一下lastid。当获取第一页数据时,因为没有上一页所以 lastid 传空或者不传,此时服务器取最新的数据即可。

获取第二页数据时,lastid 传第一页最后一条数据的ID,此时服务器取 ID < lastid 的数据,这就保证最新的评论不会影响到当前用户的分页。

这里做一个扩展,我们有时候看到某些页面在刷新的时候,会提示有多少条新的未查看评论(即列表头新的数据),这个功能的实现原理跟我们上面分页的原理差不多。

在获取第一页数据时,把第一页的第一条数据ID保存下来,后面请求每一页时都把第一条ID(firstid)带上,服务器每次查 ID > firstid 的数据条数,如果大于0即表示有新的评论。


评论列表(点赞数倒叙)

微博的评论排序也存在上面说到的分页bug,要完美解决这个需求的分页问题花费的代价(实现时间、服务器性能、存储空间等)大于功能本身,所以建议选择比较折中的方式来处理(与产品或上级沟通实现的难度)。

这个需求相比评论列表,多了点赞的功能,列表按点赞数量倒叙排列。

先说一下不严谨情况下这个分页的实现方式:

## 优先对点赞数量倒叙,再对评论ID倒叙 ##

这种方式会有两个问题:

1评论点赞数的变化导致列表排序不断改变

2新写的评论会影响列表的总量

可以沿用上面讲到的两个需求的解决方案。在解决列表排序问题上,沿用排行榜的通过变动记录表拿数据方式,增加一个表去记录评论的点赞变动记录(用空间换效率)。

优化:

1分表:(固定某个表存多少数量的数据:例如:一张表存100w的数据量);

2优化sql和建立适合的索引(复合索引);

3使用redis缓存。(redis存一份ID然后mysql存一份ID每次插入删除的时候同步即可。查询的时候只需要从redis里面找出适合的10个ID,然后到mysql里面查询出10条记录即可);

4总数要单独处理:涉及到总数 *** 作,专门维护一个总数。(例如:新注册一个会员,总数值加1,需要总数的时候直接拿这个总数,也可以在这个表上添加了触发器并创建一个专门用来统计总行数的表添加更新删除该表就会触发,分析条件后直接把统计表的相应字段累加,查询的时候直接读取统计表中的相应字段就可以了准确度没问题,如果有条件查询分页,那么分页表的数据就发挥不了左右)。

5可通过定时任务去批量查询总数,例如:开启10个线程去批量计算总数,然后再各自相加即可,不过这样会导致内存(CPU)过高,而造成内存溢出。

6修改原有界面内容,单独去查询总数,需要即去查询。也可以用ID建立一定的区间,比如查询最新的记录,每次只是查询2w条的记录。每次只要查最新的一条记录,id是自增字段,取当前的这个id值就可以大约知道总条数了(注意:项目里并不会删除参与记录),但是这种不适合带条件的查询。


首先 数据库的性能有很多种
1:速度性能
2:并发性能
3:事务性能
在速度性能上,首先你需要首先确定你的测试环境。
举个例子,比如你的软件可能的用户群是一群使用586电脑的客户,那么肯定是mysql的效率要高出oracle这个是为什么呢,mysql在事务策略和安全策略上做的工作远没有oracle做出的多。如果软件不需要这方面的高要求,那么完全可以使用mysql,这样机器的配置可以比较低,但是表现出的性能会更加优越。
前面是题外话,在测试前你需要首先顶一下你的测试环境,为了能让oracle充分的表现出他的性能,你应该选取一些强劲的小型机或者服务器来作为测试环境(虽然这样的环境对于mysql来说可能浪费了,但是这样才能确保2个数据库都充分的发挥的自己的特性)。
其次,测试访问速度,你可以通过对数据库的大批量写入来看出效果。所谓大批量写入应该尝试使用存储过程一次读入了10M的数据文件然后写入并且记录时间(同时记录cpu,内存等占用情况)。然后对于至少2个超过10万的数据表做笛卡尔积查询(全连接),查看查询的时间。

web开发中常用的分页方式,根据页码进行分页。暂且称为 Web式分页

根据页码 pageIndex 和分页大小 pageSize 进行分页。

这种分页方式,在web中使用没有什么太大问题,但是在App分页中能否套用这种分页方式呢?

App上的分页方式从表现上看,基本都是上拉加载更多形式的流式分页。如果后台接口仍然按照Web式分页方式进行设计,会有如下问题:
a、数据重复

b、数据缺失

c、offset过大时查询效率低
MySQL的limit给分页带来了极大的方便,但数据量一大的时候,limit的性能就急剧下降。
由此可见,传统Web式分页接口并不适合App分页。
3、App流式分页服务端设计

a、cursor游标式分页

优点:
1)、能够避免数据重复/遗漏
2)、limit性能不会cursor数值大小影响,性能稳定
缺点:
1)、适用于只是按照时间追加的方式的简单排序

b、按照时间分片缓存
非全量数据,只是部分热门数据,因为数据变化太快,可以基于时间段生成多个缓存。对于数据可以按时间段(5分钟)生成一个缓存分片。
具体流程如下:

此处的timestamp值,请求第1页数据时,timestamp传0,服务端检查timestamp<=0,就将当前系统时间赋值给timestamp返回,请求第2,3,n页数据时,将系统返回的timestamp传入。
缓存的key是根据timestamp进行计算的,比如5分钟一个分片,key=list_201605231700。

应用场景

比如首页热门,只是一些热门文章,排序有一定的复杂性,且相对容易变动。

目前专题中列表排序是按照点赞数排序的,分页请求

出现了重复的数据,是因为该排序是实时数据,且没有游标,无法感知前面加载的数据。

c、id列表一次性下发给App
1、请求第一页数据之前先缓存所有id列表
2、请求每页数据时,只需带入相关的id列表参数

这种方式适用于id列表不会很大(数百条数据)的业务场景,例如腾讯新闻。

前端代码块 function initTable(){ $('#test-table')bootstrapTable({ method: 'get', toolbar: '#toolbar', //工具按钮用哪个容器 striped: true, //是否显示行间隔色 cache: false, //是否使用缓存,默认为true,所以一般情况下需要设置一下


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zz/13496090.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-08-18
下一篇 2023-08-18

发表评论

登录后才能评论

评论列表(0条)

保存