TATECHATLAS
◎ 简体中文
Web 与 API

API 分页:偏移量与游标在变更数据中的应用

了解基于偏移量和基于游标的分页之间的区别以及何时使用它们,尤其是在处理频繁变化的数据库时。

本文内容

在为 API 选择基于偏移量还是基于游标的分页时,请考虑数据的性质。基于偏移量的分页(使用 LIMIT 和 OFFSET 或 page 参数)对于静态数据更简单,但在数据集频繁变化时可能导致记录丢失或重复。基于游标的分页(使用 since 或 before/after 参数)对于动态数据更健壮,因为它依赖于前一个响应中的标记,即使在添加或删除数据的情况下也能确保数据一致性。

理解基于偏移量的分页

基于偏移量的分页是一种常见方法,您可以在其中请求特定“页”的结果,或在返回有限集(LIMIT)之前跳过一定数量的记录(OFFSET)。例如,在 SQL 中,SELECT * FROM items ORDER BY id LIMIT 10 OFFSET 20 将返回第 21 到 30 条记录。API 通常使用 page=3 或 offset=20&limit=10 等查询参数来实现此功能。GitHub API 使用 page 参数来实现此目的。当数据集相对稳定时,此方法对于检索数据非常直接。

然而,当底层数据在请求之间发生变化时,基于偏移量的分页存在一个显著的缺点。如果在获取下一页之前添加了新项目或删除了现有项目,您可能会跳过记录或多次检索同一条记录。例如,如果您获取了第 1 页(记录 1-10),然后是第 2 页(记录 11-20),但在此时,新记录插入到第 5 个位置,您对第 2 页的下一次请求实际上可能会返回记录 11-20,从而有效地跳过了本应排在第 11 位的新插入记录。

```sql
-- SQL 中基于偏移量的分页示例
SELECT id, name
FROM products
ORDER BY created_at DESC
LIMIT 20 OFFSET 40; -- 跳过前 40 行并返回接下来的 20 行
```

理解基于游标的分页

基于游标的分页,也称为键集分页,它使用前一个结果集中的标记来确定下一次请求的起点。您不是指定任意偏移量,而是提供一个值(“游标”),该值代表排序数据中的特定项目或点。此游标通常源自唯一的、可排序的字段,如时间戳或上一页最后一项的 ID。

API 通常使用 after=<cursor_value> 或 since=<timestamp> 等参数来实现此功能。例如,如果上一页的最后一项的 ID 是 12345,则下一个请求可能是 GET /items?after=12345。此方法对于数据更改更具弹性,因为它始终从已知的数据点开始,确保不会遗漏任何项目,也不会重复,无论在请求之间发生何种添加或删除。

```javascript
// 基于游标的分页逻辑示例(概念性)
async function fetchNextPage(lastItemId) {
  const response = await fetch(`/api/items?after=${lastItemId}`);
  const data = await response.json();
  // data.items 包含下一组项目
  // data.nextCursor 是后续请求的游标
  return data;
}
```

基于游标的分页如何处理变更数据

基于游标的分页的关键优势在于其在动态数据集上的稳定性。当您使用游标请求数据时,API 会在其排序列表中查找与该游标对应的项目,然后返回后续项目。如果在新项目添加到游标位置之前,它们将被简单地忽略,因为起点是固定的。如果项目添加到游标之后,它们将被包含在下一页的结果中。

同样,如果删除了项目,游标仍然指向正确的后续项目。例如,如果您获取了 ID 为 100、101、102 的项目,并且游标是 102,然后删除了项目 101,使用 after=102 请求数据仍然会正确地获取 102 之后Whereas items, without any gaps or duplicates caused by the deletion.

API 实现细节:Link 头部和参数

支持分页的 API 通常在响应头部提供导航信息,特别是 Link 头部。此头部可以包含下一页、上一页、第一页和最后一页的 URL。对于基于偏移量的分页,这些 URL 通常包含 page 或 offset 参数。

基于游标的分页也可能利用 Link 头部,但 URL 将包含与游标相关的参数,如 after 或 since。一些 API 也可能直接在响应体中返回下一个游标。理解这些头部和参数对于实现健壮的分页逻辑至关重要,无论您是手动解析响应还是使用客户端库。

```http
Link: <https://api.example.com/items?page=2>; rel="next", <https://api.example.com/items?page=50>; rel="last"

Link: <https://api.example.com/items?after=item_abc>; rel="next"
```

选择正确的方法

基于偏移量的分页适用于数据基本静态或偶尔出现不一致可接受的情况。它更容易实现和理解,特别是对于显示不变的配置设置列表或很少修改的历史日志等基本用例。

基于游标的分页是处理频繁更改数据的 API(如社交媒体动态、实时仪表板或电子商务产品列表)的首选。它维护数据完整性的能力确保了用户体验的一致性,防止用户错过内容或看到重复项,这对于动态应用程序至关重要。

潜在的陷阱和注意事项

使用基于偏移量的分页时,由于服务器仍需要处理和丢弃所有跳过的行,因此较大的 OFFSET 值可能会效率低下。这可能导致性能下降。此外,如前所述,数据一致性是频繁更新数据集的主要问题。

对于基于游标的分页,游标本身必须基于一个唯一且单调递增(或递减)的列。如果排序列可能具有重复值或随时间变化(例如,可能被更新的时间戳),它仍然可能导致不一致。确保游标源自稳定、可排序的键。

使用库实现分页

许多 HTTP 客户端库和 API SDK 提供对分页的内置支持,从而抽象了许多复杂性。例如,GitHub 的 Octokit.js 库提供了 octokit.paginate() 和 octokit.paginate.iterator() 方法,可以自动处理获取多个结果页,无论它们使用偏移量还是基于游标的机制。

这些库函数通常会自动解析 Link 头部并管理后续页面的请求。在构建自己的客户端逻辑时,请务必参考特定 API 的文档,以了解它采用哪种分页策略以及如何正确提取和使用分页令牌或参数。

```javascript
// Octokit.js 获取所有 issue 的示例(处理分页)
import { Octokit } from "@octokit/rest";
const octokit = new Octokit();

async function getAllIssues(owner, repo) {
  const response = await octokit.paginate(octokit.rest.issues.listForRepo, {
    owner: owner,
    repo: repo,
    per_page: 100, // GitHub API 的最大每页数量
  });
  return response;
}

getAllIssues("octocat", "Spoon-Knife").then(issues => console.log(issues.length));
```

示例:动态数据下的偏移量与游标对比

想象一个按创建日期排序的任务列表。最初,您获取前 10 个任务(偏移量 0)。如果您在获取接下来的 10 个任务(偏移量 10)之前创建并添加到列表开头的 5 个新任务,基于偏移量的分页可能会跳过这些新任务。API 将返回最初的第 11 到第 20 个任务,而遗漏新创建的任务。

使用基于游标的分页,如果第一页的最后一个任务的创建时间戳为 T1,则您的下一个请求将是 ?after=T1。即使在 T1 之前添加了新任务,API 仍然会找到在 T1 之后创建的任务,确保您获得正确的后续项目而不会遗漏任何内容,无论插入或删除如何。

检查清单

  • 验证 API 文档以确定它使用的是基于偏移量(例如 page、offset)还是基于游标(例如 since、after、before)的分页。
  • 如果使用基于偏移量的分页处理动态数据集,请实现检查或重新获取逻辑以处理潜在的数据不一致(丢失/重复记录)。
  • 确保基于游标的分页依赖于稳定、唯一且可排序的键作为游标,以维护数据完整性。
  • 使用模拟数据更改(插入、删除)彻底测试分页,以确认所选方法按预期运行。

由于服务器需要计算和丢弃行,基于偏移量的分页对于非常大的数据集可能效率低下。基于游标的分页需要仔细实现,以确保游标键稳定且唯一。某些 API 可能不支持这两种方法,或者可能对游标格式有特定要求。

参考来源

  1. GitHub REST: pagination ↗
  2. PostgreSQL: LIMIT and OFFSET ↗
返回顶部 ↑