Web 后端的一生之敌:分页器
分页器是 Web 开发中常见的功能,看似简单的却经常隐藏着各种奇怪的坑,堪称 WEB 后端开发的一生之敌。
常见问题
边翻页边写入导致内容重复
某位用户正在浏览我的博客,他看到第一页最后一篇文章是 《Redis 缓存更新一致性》:

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

博客园使用的是时间倒序排列和limit..offset分页器,用 SQL 来描述就是:
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 接口来查询一下其中是否存在违规内容,并把违规内容删除掉。
这种实现会遇到一种问题,客户端向我们请求 10 篇文章而服务端只返回了 8 篇,甚至某一页可能一篇不剩。这些情况可能在客户端导致一些会被用户注意到的体验问题,比如上滑浏览 feed 流时出现卡顿、闪烁。
聪明的读者可能会想这个问题好办,如果请求 10 篇文章过滤后只剩下 8 篇,那我们再从数据库中取出 10 篇只要过滤后剩下 2 篇以上是不是就可以满足客户端的请求了?
好了现在问题又来了,客户端请求第一页 10 篇文章而我们已经从数据库中取出了 14 行记录,当客户端请求第二页时 offset 应为 14, 请求第三页时 offset 应为 26。。。。根据客户端发来的页码找到的 offset 是几乎不可能的事情。
分页接口通常需要告知客户端结果总数或者总页数,以便客户端判断是否到达最后一页。而使用了后置过滤的查询几乎不可能查出结果总数。
深度分页带来的性能消耗
MySQL 深度分页的性能问题以及使用自增主键优化深度分页已经广为人知,这里我们不再讨论。
与此类似,查询客户端结果总数或者总页数同样是很耗时的操作。在移动互联网时代,像博客园这样显示页码的场景已经不多,更多的是各种流,客户端并不需要知道有多少页,只需要知道是否到达最后一页即可。
解决方案
解决分页器麻烦最好的方案就是避免分页(手动滑稽
当然大多数情况无法避免分页,所以我们还是需要研究一下怎么解决上面提到的各种问题
游标分页器
游标分页器的思路和 MySQL 使用自增主键优化深度分页相同,我们不再使用 offset 表示拉取进度而是使用上次返回的最后一条结果的自增id作为游标。以上文中提到的博客重复的问题为例,若 post 表使用自增主键 id, 那么我们可以使用如下SQL 查询:
select * from posts where id < ? order by id desc limit 10;
用户浏览第一页时记住最后一篇文章《Redis 缓存更新一致性》的 id=233, 在拉取第二页时只需要进行查询:
select * from posts where id < 233 order by id desc limit 10;
游标分页器也可以解决上文提到的后置过滤的问题。客户端请求第一页 10 条内容,我们实际上从数据库中取出了 14 条,只需要将从数据库中取出的最后一条的 id 作为游标发给客户端。查询下一页时只要查询 id < cursor (升序排列时为 id > cursor) 即可。
除了自增 id 外只要是不重复的排序字段都可以作为游标,比如时间戳也可以作为游标。在无法保证时间戳不重复时我们可以使用时间戳作为整数部分、id 作为小数部分的方法来进行排序。如下面的示例代码:
// 对于时间戳相同的 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:
POST /twitter/_search?scroll=1m
{
"size": 100,
"query": {
"match" : {
"title" : "elasticsearch"
}
}
}
在查询时创建一个有效期为 1m 的快照,使用返回的 scroll id 获取下一页:
GET /_search/scroll
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
ES 真是分页器的老受害者了
Web 后端的一生之敌:分页器的更多相关文章
- 实现 Web 后端和客户端之间的分布式和认证通讯
stack.io 是一个用于实现 Web 后端和客户端之间的分布式和认证通讯. 服务器端进程之间的通讯是非常高效的,因为没有中间的代理.而来自客户端的请求通过 socket.io 进入 Node.js ...
- 8.app后端和web后端的区别
很多从web后端转到app后端的小伙伴经常很茫然,不知道这两者之间有啥区别.本文通过例子,分析web后端和app后端的区别,使各位更好地把握app后端的架构. (1) app后端要慎重考虑网络传输的流 ...
- 杂记:腾讯暑期实习 Web 后端开发面试经历
今天面试(一面)腾讯暑期实习 Web 后端开发,一言难尽. 第一部分,常规的自我介绍. 介绍完,面试官问我对人工智能有什么理解?深度学习和机器学习的区别?对调参有什么见解?语音识别中怎样运用了机器学习 ...
- Web前端 Web前端和Web后端的区分
一.绪论 1. 前台:呈现给用户的视觉和基本的操作. 后台:用户浏览网页时,我们看不见的后台数据跑动.后台包括前端.后端. 前端:对应我们写的html.css.javascript 等网页语言作用在前 ...
- 转载关于Python Web后端开发面试心得
先介绍下我的情况:通信背景,工作一年多不到两年.之前一直在做C++的MFC软件界面开发工作.公司为某不景气的国企研究所.(喏,我的工作经验很水:1是方向不对:2是行业有偏差).然后目前是在寻找Pyth ...
- Web前端和Web后端的区分
版权声明:本文为CSDN博主「十豆三展」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/zz1399590022 ...
- 单机Web后端接口服务压力测试
单机Web后端接口服务压力测试 工具:Apache jmeter 环境:Window 10 语言:Kotlin + java 架构:SpringBoot + + Mysql + redis + Spr ...
- Serverless与Web后端天生不合?
Serverless/Faas/BaaS 等概念在这几年的技术圈中是绝对的热点词汇之一,国内外众多云厂商也纷纷推出自家的 Serverless 和函数计算产品,微信也依托腾讯云推出了基于 Server ...
- 基于 Sequelize.js + Express.js 开发一套 Web 后端服务器
什么是 Sequelize 我们知道 Web 应用开发中的 Web 后端开发一般都是 Java.Python.ASP.NET 等语言.十年前,Node.js 的出现使得原本仅限于运行在浏览器中的 Ja ...
随机推荐
- i2c总线编码
i2c总线编码 发送启动信号S 在同步时钟线SCL 为高电平时,数据线出现的由高到低的下降沿. 启动信号子程序STA 1 /************************************** ...
- 洋桃电子之STM32
1.ARM内核与分类 作者:知乎用户链接:https://www.zhihu.com/question/52915983/answer/258507276来源:知乎著作权归作者所有.商业转载请联系作者 ...
- 解决Mui中popover 顶部弹出菜单弹出位置不准确以及无法收回的问题
前言 最近公司的项目转向使用Hbuilder开发移动端项目,其中想要通过在顶部标题栏加入弹出菜单的方式,来定位长列表的位置,如图所示. 问题 Mui的功能貌似还不是很完善,在使用这个弹出菜单的时候,发 ...
- java的原子类到底是啥?ABA,CAS又是些什么?
1)解决并发不是用锁就能解决吗,那SDK干嘛还要搞个原子类出来? 锁虽然能解决,但是加锁解锁始终还是对性能是有影响的,并且使用不当可能会造成死锁之类的问题. 2)原子类是怎样使用的,比如说我要实现一个 ...
- 【小程序开发】文本text-overflow属性的使用
text-overflow原本是CSS3的一个属性,在微信小程序中也支持. text-overflow文本溢出显示省略号~ 注:使用text-overflow时,需要设置固定的宽度才起作用,所以该元素 ...
- window10解决需要管理员删除文件的权限问题
1.快捷键:"win+R",输入:gpedit.msc 回车 2.依次点击:计算机配置-windows配置-安全设置-本地策略-安全选项 3.将两个管理员批准模式和管理员状态三者 ...
- 微信小程序超出后...
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
- Docker入门(windows版),利用Docker创建一个Hello World的web项目
Docker 当大家点开这篇博客的时候,相信大家对docker多多少少也有些认识了,最近学习docker这门技术,略微有些心得,写篇文章记录一下学习过程并帮大家跳过一些坑. docker的核心有两个, ...
- 32位x86处理器架构
我们看看32 位 x86 处理器的基本架构特点.这些处理器包括了 Intel IA-32 系列中的成员和所有 32 位 AMD 处理器. 操作模式 x86 处理器有三个主要的操作模式:保护模式.实地址 ...
- 《手写Mybatis》第4章:Mapper XML的解析和注册使用
作者:小傅哥 系列:https://bugstack.cn/md/spring/develop-mybatis/2022-03-20-%E7%AC%AC1%E7%AB%A0%EF%BC%9A%E5%B ...