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 ...
随机推荐
- 使用css实现任意大小,任意方向, 任意角度的箭头
使用css实现任意大小,任意方向, 任意角度的箭头 网页开发中,经常会使用到 下拉箭头,右侧箭头 这样的箭头. 一般用css来实现: { display: inline-block; margin: ...
- java中接口interface和private私有内部类怎样一块配合着用?
3.接口interface和private内部类协同工作[新手可忽略不影响继续学习]马克-to-win:由于是private内部类,外面无法访问甚至无法看到你编的源代码(如果在不同的包中),非常安全. ...
- JavaScript实现表单的校验以及匹配正则表达式
运行效果: 未填写信息报错: 匹配正则表达式: 信息校验无误: 源代码如下: 1 <!DOCTYPE html> 2 <html lang="zh"> 3 ...
- 没有高度的div中的子元素高度自动撑开
直接上代码: 很多时候 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&q ...
- 理解Promise函数中的resolve和reject
看了promise的用法,一直不明白里面的resolve和reject的用法: 运行了这两段代码之后彻底理解了promise的用法: var p = new Promise(function (res ...
- 去掉有定位的left值
left: initial; 一开始就是初始(默认值)的意思,就可以解决定位的left啦
- 深度学习(三)之LSTM写诗
目录 数据预处理 构建数据集 模型结构 生成诗 根据上文生成诗 生成藏头诗 参考 根据前文生成诗: 机器学习业,圣贤不可求.临戎辞蜀计,忠信尽封疆.天子咨两相,建章应四方.自疑非俗态,谁复念鹪鹩. 生 ...
- Bootstarp框架用法
Bootstrap框架 Bootstrap框架 2.X 3.X 4.X # 推荐使用3.X版本 使用框架调整页面样式一般都是操作标签的class属性即可 bootstrap需要依赖于jQuery才能正 ...
- C++篇:第八章_类_知识点大全
C++篇为本人学C++时所做笔记(特别是疑难杂点),全是硬货,虽然看着枯燥但会让你收益颇丰,可用作学习C++的一大利器 八.类 (一)类的概念与规则 "子类"和"子类型& ...
- [python][flask] Jinja 模板入门
Flask 和 Django 附带了强大的 Jinja 模板语言. 对于之前没有接触过模板语言的人来说,这类语言基本上就是包含一些变量,当准备渲染呈现 HTML 时,它们会被实际的值替换. 这些变量放 ...