任何返回一个 collection 的 endpoint 都需要限制并塑形结果。三个关注点——分页、过滤、排序——都住在 query string 里。
分页:offset vs cursor
Offset(基于页) —— ?page=3&limit=20 或 ?offset=40&limit=20。
GET /orders?offset=40&limit=20 HTTP/1.1
简单且允许跳到任意页,但在规模大时有两个问题:OFFSET 100000 迫使数据库扫描并丢弃 10 万行(慢),而且如果在两次请求之间有行被插入/删除,条目会移位,你会看到重复或跳过。
Cursor(keyset) —— client 传递一个指向已看到的最后一个条目的不透明指针。
GET /orders?limit=20&cursor=eyJpZCI6MTAyMH0 HTTP/1.1
HTTP/1.1 200 OK
{
"data": [ /* 20 个 order */ ],
"page": { "next_cursor": "eyJpZCI6MTA0MH0", "has_more": true }
}
server 把 cursor 翻译成 WHERE id > 1020 ORDER BY id LIMIT 20——一次用索引的查找,在任何深度都保持快速,并且对插入是稳定的。取舍:无法随机访问“第 500 页”。
Offset → small datasets, admin UIs, "jump to page N"
Cursor → large/infinite feeds, real-time data, mobile scroll
GET /orders?status=shipped&min_total=100&sort=-created_at,id HTTP/1.1
status=shipped)。把允许的字段做成白名单。sort param 排序;- 前缀表示降序(-created_at)。支持多键以获得稳定排序。limit 设上限(例如最多 100),这样 client 就不能请求一百万行。这是最实用的 API 设计题之一,因为几乎每个列表 endpoint 都会遇到它,且选择有真实的性能后果。面试官通常在探offset-vs-cursor 的取舍:一个到处用 offset 分页的候选人,没有体会过 OFFSET 1000000 扫描整表的痛,或者那个“结果移位”的 bug——用户滚动一个无限 feed,因为有新条目被插入而看到同一条帖子两次。Cursor 分页通过把查询变成一次用索引的 keyset 查找来修复这两点,这就是为什么每个大规模 API(Twitter、Stripe、Slack)都用它来做 feed。强的回答还会提到运营护栏——把过滤/排序字段做白名单(避免无索引查询和注入)以及给 limit 设上限(保护数据库)——这把一个在生产中运营过这些 endpoint 的人,与一个只读过它们的人区分开来。
一个包含详细解答的 IT 面试题库——从初级到高级。
捐赠