流行的反模式:分页

您好,我叫Dmitry Karlovsky,我...我不喜欢读书,因为当您翻页时,您会发现一个有趣的故事。 当您忘记上一页的最后一句以什么结尾时,您必须回滚以重新阅读它,这有点犹豫。 而且,如果对实物书籍并不那么害怕,那么随着发布备用服务器,一切都会变得非常可悲-毕竟,页面上现在有一些数据,一秒钟之后,情况就完全不同了。 让我们考虑一下它是如何发生的,应该归咎于谁,最重要的是,该怎么做。


杂项惩罚者


问题


因此,我们需要为查询“分页”发出所有消息,从最新消息( 最后一个消息从top更改 )开始或以某种棘手的顺序发出 。 只要这些消息少于一百条,一切都很好-我们只需从数据库中进行选择并返回数据即可:


客户要求:


GET /message/text=/ 

数据库请求:


 SELECT FROM Message WHERE text LICENE "" ORDER BY changed DESC 

客户端的JSON响应方案:


 Array<{ id : number , text : string }> 

但是消息的数量正在增长,我们遇到了以下麻烦:


  1. 随着更多数据的收集,数据库查询变得越来越慢。
  2. 通过网络发送数据的时间越来越长。
  3. 在客户端上呈现此数据的时间越来越长。

从某个阈值开始,延迟变得如此之大,以至于无法使用我们的网站。 当然,如果他还没有躺下,就对大量并行的繁重请求感到厌倦。


最简单的解决方案,也许首先想到的是,您现在可以在任何烤面包机中使用它-分发的数据不是全部,而是分成几页。 我们需要做的只是从客户端向数据库请求中抛出一个附加参数:


 GET /message/text=/page=5/ 

 SELECT FROM Message WHERE text LICENE "" ORDER BY changed DESC SKIP 5 * 10 LIMIT 10 

 SELECT count(*) FROM Message WHERE text LICENE "" 

 { pageItems : Array<{ id : number , text : string }> totalCount : number } 

是的,是的,我们仍然必须重新计算所有消息,以便客户端可以绘制页面列表或计算虚拟滚动的高度,但是至少我们不需要从数据库中获取所有这些100500条消息。


如果我们有一个很受欢迎的论坛已经很长时间不再相关,那么一切都会很好。 但是他们写信给我们,写信给用户,当用户阅读第五页时,消息列表发生了不可识别的变化:添加了新消息,删除了旧消息。 因此,从用户的角度来看,我们会遇到两种类型的问题:


  1. 在下一页上,可能会再次出现上一页已经出现的消息。
  2. 用户完全看不到某些消息,因为它们设法在用户从5过渡到6的过程中准确地从第6页移动到第5页。

此外,我们仍然存在性能问题。 每次跳转到下一页都会导致一个事实,即我们需要在数据库中执行多达两个搜索查询,而前一页中跳过的元素越来越多。


是的,在客户端进行有效的实现并不是那么简单-您必须为任何服务器响应都可以返回新的消息总数这一事实做好准备,这意味着如果当前页面突然出现,我们将需要重新绘制分页器并重定向到另一页是空的。 当然,如果出现重复,您也不会落伍。


另外,有时客户端需要更新搜索结果,但是负载仍将接收先前请求中可能已经拥有的数据。


如您所见,分页有很多问题。 真的没有更好的解决方案吗?


解决方案


首先,请注意,在使用数据库时,有2个操作本质上是不同的:


  1. 搜索。 查找某些查询的数据指针的操作相对繁重。
  2. 采样。 实际获取数据的相对简单的操作。

这将是理想的:


  1. 一旦搜索并在某个时间点以快照的形式记住其结果。
  2. 根据需要快速选择小部分数据。

在哪里存储快照? 有2个选项:


  1. 在服务器上。 但是随后,我们将其与大量垃圾堆一起塞入垃圾桶,并且需要随着时间的流逝清理搜索结果。
  2. 给客户。 但是,那么您必须立即将所有快照传输到客户端。

让我们估计快照的大小,它只是标识符列表。 令人怀疑的是,用户是否耐心地滚动了至少100页而不使用过滤和排序。 假设每页有20个元素。 每个标识符在json表示中将占用不超过10个字节。 乘以不超过20kb。 而且很可能少得多。 对输出大小设置硬限制(例如1000个元素)是合理的。


 GET /message/text=/ 

 SELECT id FROM Message WHERE text LICENE "" ORDER BY changed DESC LIMIT 1000 

 Array<number> 

现在,客户可以绘制至少一个分页器,至少一个虚拟卷轴,仅请求数据以获取他感兴趣的标识符。


 GET /message=49,48,47,46,45,42,41,40,39,37/ 

 SELECT FROM Message WHERE id IN [49,48,47,46,45,42,41,40,39,37] 

 Array< { id : number , text : string } | { id : number , error : string } > 

我们最终得到的是:


  1. 规范化的API:分别搜索,分别选择数据。
  2. 减少搜索查询的数量。
  3. 您不能请求已下载的数据,也不能在后台对其进行更新。
  4. 客户端上相对简单且通用的代码。

在这些缺点中,除以下几点外,可以指出:


  1. 为了显示内容,用户需要至少发出2个连续的请求。
  2. 当标识符存在并且其数据不再可用时,有必要处理这种情况。

Source: https://habr.com/ru/post/zh-CN413133/


All Articles