Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Web 后端的一生之敌:分页器

Web 后端的一生之敌:分页器

作者头像
Java3y
发布于 2024-03-25 08:19:18
发布于 2024-03-25 08:19:18
21300
代码可运行
举报
文章被收录于专栏:Java3yJava3y
运行总次数:0
代码可运行

作者:finley 出处:https://www.cnblogs.com/Finley/p/16286123.html 版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

分页器是 Web 开发中常见的功能,看似简单的却经常隐藏着各种奇怪的坑,堪称 WEB 后端开发的一生之敌。

常见问题

边翻页边写入导致内容重复

某位用户正在浏览我的博客,他看到第一页最后一篇文章是 《Redis 缓存更新一致性》:

在他浏览第一页的过程中,我发布了一篇新文章。他继续浏览,发现第二页的第一篇文章仍然是 《Redis 缓存更新一致性》:

博客园使用的是时间倒序排列和limit..offset分页器,用 SQL 来描述就是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select * from posts where user_id = ? order by publish_time desc limit 10 offset 10;

在用户浏览第一页时《Redis 缓存更新一致性》按时间倒序排列在第 10 位,当发布新文章后它被挤到了第 11 位。读者使用 limit 10 offset 10 查询第二页时它便会再次出现。

上述情况只是在浏览过程中在头部追加了新的数据,在搜索引擎这类条件很多、排序算法复杂的场景中,第一次查询和第二次查询的顺序可能完全不同,分页器也难以实现。

后置过滤

一般情况下我们可以使用 where 语句过滤出我们需要的记录,然而在工作中也经常碰到 MySQL 不能完成所有过滤的情况

比如我们需要在返回结果前调用一下 rpc 接口来查询一下其中是否存在违规内容并把违规内容过滤掉。

或者有朋友在 mysql 中存储了 json 字符串而且使用的是 MySQL 5.7 之前的版本,只能在业务逻辑中解析 json 并进行过滤了。

后置过滤会遇到一种问题,客户端向我们请求 10 篇文章而服务端过滤后只剩下了 8 篇甚至某一页可能一篇不剩。这可能会在客户端导致一些会被用户注意到的体验问题,比如上滑浏览 feed 流时出现卡顿、闪烁。

聪明的读者可能会想这个问题好解决,如果请求 10 篇文章过滤后只剩下 8 篇,那我们再从数据库中取出 10 篇只要过滤后剩下 2 篇以上是不是就可以满足客户端的请求了?

ok, 我们照此实现,于是问题又来了。客户端请求第一页 10 篇文章而我们已经从数据库中读到了第 14 行,所以客户端请求第二页时 offset 应为 14。依次类推请求第 3 页时 offset 应为 26, 第 4 页的 offset 应为 44。。。。根据客户端发来的页码找到的 offset 是几乎不可能的事情

另一个问题是分页接口通常需要告知客户端结果总数或者总页数以便客户端判断是否到达最后一页,而使用了后置过滤的查询几乎不可能查出结果总数,emmm

深度分页带来的性能消耗

MySQL 深度分页的性能问题以及使用自增主键优化深度分页已经广为人知,这里我们不再讨论。

与此类似,查询客户端结果总数或者总页数同样是很耗时的操作。在移动互联网时代像博客园这样显示页码的场景已经不多,更多的是各种样式的信息流。客户端并不需要知道有多少页只需要知道是否到达最后一页即可, 这为我们优(tao)化(ke)留下很大空间。

解决方案

解决分页器麻烦最好的方案就是避免分页

当然大多数情况无法避免分页,所以我们还是需要研究一下怎么解决上面提到的各种问题

游标分页器

游标分页器的思路和 MySQL 使用自增主键优化深度分页相同,我们不再使用 offset 表示拉取进度而是使用上次返回的最后一条结果的自增 id 作为游标。

以上文中提到的博客重复的问题为例,若 post 表使用自增主键 id, 那么我们可以使用如下SQL 查询:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select * from posts where id < ? order by id desc limit 10;

用户浏览第一页时记住最后一篇文章《Redis 缓存更新一致性》的 id=233, 在拉取第二页时只需要进行查询:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select * from posts where id < 233 order by id desc limit 10;

游标分页器也可以解决上文提到的后置过滤的问题。客户端请求第一页 10 条内容,我们实际上从数据库中取出了 14 条,只需要将从数据库中取出的最后一条的 id 作为游标发给客户端。查询下一页时只要查询 id < cursor (升序排列时为 id > cursor) 即可。

除了自增 id 外只要是不重复的排序字段都可以作为游标,比如时间戳也可以作为游标。在无法保证时间戳不重复时我们可以使用时间戳作为整数部分、id 作为小数部分的方法来构造不会重复的时间戳。如下面的示例代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 对于时间戳相同的 post 我们并不关心谁前谁后,我们只要求排序稳定
// 若 post1.CreatedAt == post2.CreatedAt,查询第一页时 post1 在前 post2 在后,查询第二页时变成了 post2 在前 post1 在后,那么 post1 会出现两次,post2 会被漏掉
// 所以我们需要查询结果是稳定的,post1 始终在 post2 之前或者 post2 始终在 post1 之前
func GetUniqueTime(post *Post) float64 {
 intPart := strconv.FormatInt(post.CreatedAt.Unix(), 10)
 decimalPart := strconv.FormatUint(post.ID, 10) // 只要求 ID 唯一,并不要求 ID 有序
 str := intPart + "." + decimalPart
 f, _ := strconv.ParseFloat(str, 64)
 return f
}

能使用游标分页器的数据库也不仅限于 MySQL 等关系型数据库,Redis 的 SortedSet 或者 ElasticSearch 的 search_after 都可以使用游标分页器。

游标分页器中不再有具体的页码概念也不再需要总页数,只需要知道当前是否为最后一页即可。我们可以在查询数据库时可以将 limit 加 1 来方便地判断当前是否是最后一页。

比如客户端请求 10 篇文章,我们查询数据库时 limit 设为 11,若数据库返回 11 条记录说明还有下一页,若数据库返回 10 条或 10 条以下的记录则说明当前已到最后一页。

limit 加 1 的目的是为了避免最后一页恰好有 10 条记录的情况,若 limit = 10 且数据库返回 10 条记录我们会认为还有下一页,而客户端继续查询下一页时只能返回空结果。这不仅会空耗资源更重要的是可能会出现一些体验上的问题,比如客户端提示「上滑加载更多」而用户上滑后并无新内容出现的尴尬局面。

游标分页器只适用于元素之间的相对顺序(即A始终在B前)不会发生改变,结果集中只会插入新元素或删除部分元素的情况

快照

对于搜索引擎这种两次查询中相对顺序可能发生改变的场景,游标分页器也无能为力。若无法避免分页则只能采取快照的方式,在搜索完毕后将整个搜索结果缓存下来,拉取后续内容时不重新搜索而是拉取快照的剩余内容

使用快照的典型的例子是 ElasticSearch 的 Scroll API:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
POST /twitter/_search?scroll=1m
{
    "size": 100,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}

在查询时创建一个有效期为 1m 的快照,使用返回的 scroll id 获取下一页:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET /_search/scroll
{
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

ES 真是分页器的老受害者了

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java3y 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
ElasticSearch分页查询的3个坑
官方已经不再推荐采用Scroll API进行深度分页。如果遇到超过 10000 的深度分页,推荐采用search_after + PIT。
公众号 IT老哥
2022/09/19
4.8K1
ElasticSearch分页查询的3个坑
深度分页问题
深度分页问题通常出现在数据量非常大的情况下,使用传统的 `LIMIT offset, count` 方式会导致性能问题,因为数据库需要扫描大量数据才能找到偏移量。这个解决方案是使用基于游标的分页(Cursor-based Pagination),通过记录上一页的最后一条记录的ID来实现高效分页。
JDK7.0
2025/03/12
1500
业界难题-“跨库分页”的四种方案
一、需求缘起 分页需求 互联网很多业务都有分页拉取数据的需求,例如: (1)微信消息过多时,拉取第N页消息 (2)京东下单过多时,拉取第N页订单 (3)浏览58同城,查看第N页帖子 这些业务场景对应的消息表,订单表,帖子表分页拉取需求有这样一些特点: (1)有一个业务主键id, 例如msg_id, order_id, tiezi_id (2)分页排序是按照非业务主键id来排序的,业务中经常按照时间time来排序order by 在数据量不大时,可以通过在排序字段time上建立索引,利用SQL提供的offse
架构师之路
2018/03/01
9K6
业界难题-“跨库分页”的四种方案
老弟想自己做个微信,被我一个问题劝退了。。
大家好,我是程序员鱼皮。最近老弟小阿巴放暑假,想找点事情做,于是就来问我:老鲏,我想做个练手项目,有没有什么好的建议?
程序员鱼皮
2024/08/09
1790
老弟想自己做个微信,被我一个问题劝退了。。
百亿级数据 分库分表 后怎么分页查询?
随着数据的日益增多,在架构上不得不分库分表,提高系统的读写速度,但是这种架构带来的问题也是很多,这篇文章就来讲一讲跨库/表分页查询的解决方案。
码猿技术专栏
2023/05/01
2.5K0
百亿级数据 分库分表 后怎么分页查询?
Elasticsearch中的三种分页策略深度解析:原理、使用及对比
from + size是Elasticsearch中最直观的分页方式。其中,from参数表示从第几条记录开始返回,size参数表示返回的记录数。
公众号:码到三十五
2024/05/13
2.2K0
Elasticsearch中的三种分页策略深度解析:原理、使用及对比
elasticsearch的分页查询的用法与分析
前言:在接口设计上,对数据进行查询时,往往会采用分页查询的形式进行数据的拉取,主要是为了避免一次性返回过大的结果导致对网络,内存,客户端应用程序,集群服务等产生过大的压力,导致出现性能问题。在elasticsearch中分页查询主要有两种方式,from size分页查询与scroll深度分页查询。
空洞的盒子
2023/11/21
1.7K2
微服务设计原则——高性能
对于查询 API 来说,当查询结果集包含成千上万条记录时,返回所有结果是一个挑战,它给服务器、客户端和网络带来了不必要的压力,于是便有了分页接口。
恋喵大鲤鱼
2024/08/25
1370
mysql分页读取数据重复问题
服务端开发过程中,我们通常需要与mysql数据库进行数据交互。在大多数情况下,由于数据量过大、网络时延、mysql参数配置限制,以及业务逻辑的限制等,需要我们对所需的数据进行分页读取。尤其是需要读取的数据量过大时,我们经常会遇到下面这种错误类型。
闻说社
2024/12/02
4140
mysql分页读取数据重复问题
如何解决MySQL 的深度分页问题?
在构建高性能、可扩展的 Web 应用程序时,数据库查询性能往往是影响整体系统响应速度的关键因素之一。尤其是在处理大规模数据时,如何高效地进行分页查询成为了开发者需要重点关注的问题。本文将深入探讨 MySQL 中 LIMIT ... OFFSET ... 语法带来的性能挑战,并介绍一种更高效的解决方案——游标分页方法(Cursor Pagination)。
每周聚焦
2025/01/07
2790
如何解决MySQL 的深度分页问题?
如何解决MySQL order by limit语句的分页数据重复问题?
在MySQL中我们通常会采用limit来进行翻页查询,比如limit(0,10)表示列出第一页的10条数据,limit(10,10)表示列出第二页。
好好学java
2021/04/30
3.2K0
DQL-limit分页
在我们使用查询语句的时候,经常要返回前几条或者中间某几行数据,这个时候怎么办呢?不用担心,mysql已经为我们提供了这样一个功能-limit。
星哥玩云
2022/09/15
4550
DQL-limit分页
mysql 5.6 order by limit 排序分页数据重复问题
https://mariadb.com/kb/en/filesort-with-small-limit-optimization/
明明如月学长
2021/08/31
1.2K0
Elasticsearch:使用search after实现深度分页
对于大量的数据而言,我们尽量避免使用 from+size 这种方法。这里的原因是 index.max_result_window 的默认值是 10K,也就是说 from+size 的最大值是1万。搜索请求占用堆内存和时间与 from+size 成比例,这限制了内存。 为了避免过度使得我们的 cluster 繁忙,通常 Scroll 接口被推荐作为深层次的 scrolling,但是因为维护 scroll 上下文也是非常昂贵的,所以这种方法不推荐作为实时用户请求。 Elasticsearch:使用from+si
IT大咖说
2022/07/26
9.7K0
Elasticsearch:使用search after实现深度分页
RESTful API 规范 v1.0
由以上例子可以看出_link就是以Hyperlink表述资源与资源之间的关系,这种方式使客户端与服务端能很好的分离开来,只要接口的定义不变,客户端与服务端就可以独立的开发和演变。
IMWeb前端团队
2019/12/04
7670
如何解决MySQL order by limit语句的分页数据重复问题?
在MySQL中我们通常会采用limit来进行翻页查询,比如limit(0,10)表示列出第一页的10条数据,limit(10,10)表示列出第二页。
JAVA葵花宝典
2021/04/08
1.5K0
灵魂两问:MySQL分页有什么性能问题?怎么优化?
在这种建表语句中不用过度注重细节,只需要知道 id 是主键,并且在user_name建了一个非主键的索引就行了。
xiao李
2024/02/03
8550
灵魂两问:MySQL分页有什么性能问题?怎么优化?
基于游标的分页接口实现
分页接口的实现,在偏业务的服务端开发中应该很常见,PC时代的各种表格,移动时代的各种feed流、timeline。
贾顺名
2019/12/09
1.8K0
基于游标的分页接口实现
一起学Elasticsearch系列-深度分页问题
ES的深度分页问题指的是在大数据集和大页数的情况下,通过持续向后翻页来获取查询结果的一种性能问题。当页码非常高时,ES需要遍历大量文档才能找到正确的分页位置,导致性能和查询速度变慢。
BookSea
2023/12/28
7320
一起学Elasticsearch系列-深度分页问题
API 分页探讨:offset 来分页真的有效率?
对于设计和实现 API 来说,当结果集包含成千上万条记录时,返回一个查询的所有结果可能是一个挑战,它给服务器、客户端和网络带来了不必要的压力,于是就有了分页的功能。
二哥聊运营工具
2021/12/17
1.4K0
API 分页探讨:offset 来分页真的有效率?
相关推荐
ElasticSearch分页查询的3个坑
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验