2026-08-24 17:14:31.177144+08 / restful-api
Pagination
介绍 RESTful API 中 Offset 分页、Cursor 分页、稳定排序、过滤一致性和页面大小控制等设计规范。
Pagination
当 API 暴露的资源集合可能包含数百个以上的元素时,应使用分页。分页可以避免服务消耗过多资源,并为客户端遍历大型数据集提供可预测且高效的方式。
大型集合必须支持分页
可能包含大量元素的资源集合必须支持分页。
分页可以:
- 避免过度消耗内存、数据库和网络资源
- 减少响应延迟和载荷大小
- 保护服务,避免过载
- 改善客户端遍历和批处理
- 避免一次请求返回无界数量的资源
分页请求成功处理后应返回 200 OK。
两种常见策略是:
- Offset 分页:使用数字偏移量标识第一个待返回元素的位置。
- Cursor 分页:使用不透明 Cursor 标识结果集中的位置。
具体采用哪种分页机制属于 API 实现决策,应根据底层数据特征和客户端使用场景选择。
分页查询参数
分页参数应使用一致且约定俗成的名称。
Offset 分页示例:
GET /resources?offset=100&limit=50
常见参数:
| 参数 | 描述 |
|---|---|
| offset | 返回结果前需要跳过的元素数量 |
| limit | 最多返回的元素数量 |
Cursor 分页示例:
GET /resources?cursor=eyJpZCI6IjEyMzQ1In0&limit=50
| 参数 | 描述 |
|---|---|
| cursor | 标识集合中某个位置的不透明指针 |
| limit | 最多返回的元素数量 |
客户端不得检查、修改或自行构造 Cursor。Cursor 应被视为由服务端生成的不透明值。
提供分页导航链接
分页响应在适用时应提供导航链接。
常见链接关系包括:
- self:当前页面
- first:第一页
- prev:上一页
- next:下一页
- last:最后一页
导航链接可以减少客户端实现分页逻辑的工作量,并允许服务端控制页面地址的表达方式。
典型响应可以包含 totalCount、self、first、prev、next、last、query 和 items 字段。
响应结构与 API 内部使用 Offset 分页还是 Cursor 分页无关。
响应结构
分页集合通常应将当前页返回的资源放入 items 数组中。
{
"items": [
{
"id": "resource-501",
"name": "Resource 501"
},
{
"id": "resource-502",
"name": "Resource 502"
}
]
}
还可以增加分页元数据。
totalCount
totalCount 表示符合请求条件的资源总数。它是可选的,因为对某些数据存储来说,计算精确总数可能成本很高或不可行。
isLast
可选的 isLast 属性可以表示当前响应是否为最后一页。当 API 有意不计算 totalCount 时,这个字段很有用。
query
query 对象可以包含应用于集合的过滤条件,例如 status 和 region。
Offset 分页
Offset 分页使用整数位置和页大小。
GET /resources?offset=500&limit=100
服务端跳过前 500 个匹配资源,最多返回 100 个资源。
优点
Offset 分页:
- 容易理解
- 容易实现
- 得到数据库和框架的广泛支持
- 适合客户端需要页码的场景
- 适合用户直接跳转到任意页面的场景
例如,包含 Previous、1、2、3、...、51、...、100、Next 的界面通常自然对应 Offset 分页。
局限
对于非常大的数据集,Offset 分页可能效率较低。例如 OFFSET 500000 的查询可能要求数据库在返回目标页面前定位或处理数十万条记录。
这在超大数据集、分布式数据库、分片数据库、NoSQL 数据库和频繁变化的数据集中尤其明显。
当集合在两次请求之间发生变化时,Offset 分页还可能产生不一致结果。例如客户端先读取 A、B、C,随后集合开头插入 X,偏移位置就会发生变化,客户端可能收到重复元素或完全跳过某些元素。
Cursor 分页
Cursor 分页使用表示结果集位置的不透明指针。
GET /resources?cursor=eyJpZCI6IjUwMCJ9&limit=100
Cursor 内部可能包含最后返回资源的标识符、排序键、分页方向、查询过滤条件以及快照或一致性信息。但这些实现细节不得成为客户端契约的一部分。
客户端只能使用服务端返回的 Cursor。
Cursor 的不透明性
Cursor 必须被视为不透明值。客户端不得:
- 解析 Cursor
- 修改 Cursor
- 生成 Cursor
- 依赖 Cursor 的内部表示
即使 Cursor 恰好是经过 Base64 编码的 JSON,客户端也不得解码后自行构造新的 Cursor。服务端应能够自由调整 Cursor 实现,而不破坏 API 消费者。
优点
Cursor 分页特别适合:
- 大型数据集
- 持续变化的数据集
- 事件流
- 活动流
- 分布式或分片数据库
- NoSQL 数据库
- 客户端通常按顺序遍历的 API
它还可以避免深层 Offset 分页带来的日益增加的数据库偏移成本。服务端内部可以使用 Keyset 分页,例如根据最后一个 ID 查询下一页,从而更好地利用索引。
局限
当客户端需要直接跳转到任意页面时,Cursor 分页不太适合。Cursor 通常表示相对于已知位置的导航;如果不知道第 51 页的 Cursor,客户端就无法直接构造该位置。
在天然使用 Cursor 分页的系统中,精确总数也可能成本很高或无法获得。
在 Offset 和 Cursor 分页之间选择
没有一种方式在所有场景下都更好,应根据 API 需求选择。
| 需求 | Offset | Cursor |
|---|---|---|
| 实现简单 | 是 | 部分适合 |
| 框架支持广泛 | 是 | 部分适合 |
| 跳转到任意页面 | 是 | 否 |
| 显示页码 | 是 | 部分适合 |
| 大型数据集 | 部分适合 | 是 |
| 频繁变化的数据集 | 部分适合 | 是 |
| 分布式或分片存储 | 部分适合 | 是 |
| 顺序遍历 | 是 | 是 |
| 精确总数 | 是 | 部分适合 |
| 插入或删除期间保持稳定遍历 | 部分适合 | 是 |
一般建议:
对相对稳定且适合页码导航的数据集使用 Offset 分页。
对大型或频繁变化的数据集使用 Cursor 分页,此时高效顺序遍历比任意页面跳转更重要。
稳定排序
分页要求确定性的排序,因此分页集合应定义明确且稳定的排序顺序。
只按非唯一字段排序可能导致不稳定的页面边界。例如多个资源拥有相同时间戳时,它们的相对顺序可能在不同请求之间变化。
稳定实现应使用唯一的次级键,例如:
ORDER BY created_at, id
此时 Cursor 可以逻辑上表示 createdAt 和 id 的组合,而不是只表示 createdAt,从而显著降低遍历过程中出现重复或遗漏资源的风险。
过滤与分页
分页必须与过滤保持一致。
例如:
GET /resources?status=active®ion=eu&limit=100
如果该查询返回了 Cursor,后续请求必须继续访问同一个逻辑结果集。Cursor 可以绑定过滤值、排序配置、分页方向以及必要时的页大小。
服务端应拒绝无效或不兼容的 Cursor,而不是静默返回无关结果集。
页面大小
API 应定义合理的默认页大小,并限制最大页大小。
例如:
Default limit: 100
Maximum limit: 1000
客户端可以请求不同大小,例如 limit=50,但服务端应强制执行上限,防止客户端通过 limit=100000000 之类的请求绕过分页。
具体限制取决于资源大小、查询复杂度、基础设施能力和服务等级要求。
总数统计的注意事项
返回 totalCount 对客户端可能很有用,但 API 不应假设计算精确总数总是廉价的。
对于大型或分布式数据集,COUNT(*) 可能比读取单页数据昂贵得多。因此,除非 API 契约或用户体验明确要求,否则 totalCount 应视为可选字段。
Cursor API 尤其可以有意省略总数。对于顺序遍历,next 链接的存在或缺失可能已经足够。
判断最后一页
API 可以通过省略 next 链接来表示集合结束,也可以显式返回 isLast: true。具体表示方式应在 API 内保持一致。
客户端应优先遵循服务端提供的导航信息,而不是自行计算下一页位置。
推荐设计
分页响应可以包含 totalCount、isLast、self、first、prev、next、last、query 和 items 等字段。但并非所有字段都是必需的。
许多 API 使用更小的表示就足够,例如只返回 next 和 items。设计应避免不必要的元数据,同时为客户端可靠导航集合提供足够信息。
分页设计规则总结
- 暴露潜在大型集合的 API 必须支持分页。
- 成功处理分页集合请求时必须返回 200 OK。
- 在适用时,应提供 self、next、prev、first、last 等分页导航链接。
- 返回的资源通常应放在 items 数组中。
- 当精确集合大小有用且计算成本合理时,可以提供 totalCount。
- API 应定义默认页大小和最大页大小。
- Cursor 对 API 客户端必须是不透明的。
- 客户端不得检查、修改或构造 Cursor。
- 分页查询应使用确定且稳定的排序。
- Offset 分页通常适合稳定数据集和页码导航。
- Cursor 分页通常更适合大型、动态、分布式或持续变化的数据集。
- 分页行为应与过滤和排序参数保持一致。
- API 分页和 UI 分页相关但彼此独立,API 不应假设特定的用户界面分页模型。