2026-08-24 17:14:31.177144+08 / restful-api

Pagination

介绍 RESTful API 中 Offset 分页、Cursor 分页、稳定排序、过滤一致性和页面大小控制等设计规范。

Restful ApiApi DesignOpenapi

Pagination

当 API 暴露的资源集合可能包含数百个以上的元素时,应使用分页。分页可以避免服务消耗过多资源,并为客户端遍历大型数据集提供可预测且高效的方式。

大型集合必须支持分页

可能包含大量元素的资源集合必须支持分页。

分页可以:

  • 避免过度消耗内存、数据库和网络资源
  • 减少响应延迟和载荷大小
  • 保护服务,避免过载
  • 改善客户端遍历和批处理
  • 避免一次请求返回无界数量的资源

分页请求成功处理后应返回 200 OK。

两种常见策略是:

  • Offset 分页:使用数字偏移量标识第一个待返回元素的位置。
  • Cursor 分页:使用不透明 Cursor 标识结果集中的位置。

具体采用哪种分页机制属于 API 实现决策,应根据底层数据特征和客户端使用场景选择。


分页查询参数

分页参数应使用一致且约定俗成的名称。

Offset 分页示例:

http
GET /resources?offset=100&limit=50

常见参数:

参数 描述
offset 返回结果前需要跳过的元素数量
limit 最多返回的元素数量

Cursor 分页示例:

http
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 数组中。

json
{
  "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 分页使用整数位置和页大小。

http
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 分页使用表示结果集位置的不透明指针。

http
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 分页,此时高效顺序遍历比任意页面跳转更重要。


稳定排序

分页要求确定性的排序,因此分页集合应定义明确且稳定的排序顺序。

只按非唯一字段排序可能导致不稳定的页面边界。例如多个资源拥有相同时间戳时,它们的相对顺序可能在不同请求之间变化。

稳定实现应使用唯一的次级键,例如:

text
ORDER BY created_at, id

此时 Cursor 可以逻辑上表示 createdAt 和 id 的组合,而不是只表示 createdAt,从而显著降低遍历过程中出现重复或遗漏资源的风险。


过滤与分页

分页必须与过滤保持一致。

例如:

http
GET /resources?status=active&region=eu&limit=100

如果该查询返回了 Cursor,后续请求必须继续访问同一个逻辑结果集。Cursor 可以绑定过滤值、排序配置、分页方向以及必要时的页大小。

服务端应拒绝无效或不兼容的 Cursor,而不是静默返回无关结果集。


页面大小

API 应定义合理的默认页大小,并限制最大页大小。

例如:

text
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。设计应避免不必要的元数据,同时为客户端可靠导航集合提供足够信息。


分页设计规则总结

  1. 暴露潜在大型集合的 API 必须支持分页。
  2. 成功处理分页集合请求时必须返回 200 OK。
  3. 在适用时,应提供 self、next、prev、first、last 等分页导航链接。
  4. 返回的资源通常应放在 items 数组中。
  5. 当精确集合大小有用且计算成本合理时,可以提供 totalCount。
  6. API 应定义默认页大小和最大页大小。
  7. Cursor 对 API 客户端必须是不透明的。
  8. 客户端不得检查、修改或构造 Cursor。
  9. 分页查询应使用确定且稳定的排序。
  10. Offset 分页通常适合稳定数据集和页码导航。
  11. Cursor 分页通常更适合大型、动态、分布式或持续变化的数据集。
  12. 分页行为应与过滤和排序参数保持一致。
  13. API 分页和 UI 分页相关但彼此独立,API 不应假设特定的用户界面分页模型。
Pagination | Remi Resume