4. 分页参数设计

行情接口的分页参数看似只是“翻页”,实际上它同时决定了数据一致性、查询性能、限流成本和重试语义。设计不当会出现重复K线、漏数据、深分页拖垮数据库,甚至在重试时把同一页数据反复拉取。本节把常见分页参数分成三组:页码型、偏移型、游标/时间型,并给出边界处理与选型建议。

一、三组分页参数的语义差异

页码型以 page 与 page_size 为核心,语义是“第几页、每页多少条”。它对人友好,适合后台管理页面,但在行情场景中风险最大:当新数据不断写入头部时,第 2 页的内容会整体后移,导致重复或遗漏。偏移型使用 offset 与 limit,语义是“跳过多少条、取多少条”,本质与页码型相同,只是换了一种表达;offset 越大,数据库扫描成本越高。游标/时间型使用 cursor、since、until,语义是“从某个位置之后继续取”,天然抗数据插入,适合增量同步与高频轮询。

关键判断:如果接口返回的是“持续追加”的行情数据,优先使用 cursor 或 since/until;page/page_size 只用于静态或低频快照查询。

二、参数语义与边界处理清单

参数 语义 典型边界 处理建议
page 第几页,从1开始 0、负数、超大值 小于1时归一为1;超过总页数返回空数组而非报错
page_size 每页条数 0、负数、超过上限 默认20,最大200;超限截断并返回实际值
offset 跳过条数 负数、超过总量 负数归零;建议限制最大 offset,避免深分页
limit 返回条数 0、超大值 与 page_size 共用上限;0 可视为取默认值
cursor 上一页最后一条的位置标识 过期、伪造、格式错误 校验签名与有效期;无效时返回 400 并提示重新同步
since 起始时间(含) 晚于 until、未来时间 since > until 时交换或报错;未来时间返回空
until 结束时间(不含或含需明确) 早于 since、格式错误 统一为左闭右开 [since, until),避免边界重复

三、游标分页的工作流程

游标分页的核心是“位置”而不是“页码”。服务端把排序键(如时间戳+唯一ID)编码成 cursor,客户端下次请求原样带回。这样即使期间有新数据插入,也不会影响已翻过的位置。

GET /v1/klines?symbol=BTCUSDT&interval=1m&limit=100
{
  "data": [ ... ],
  "next_cursor": "eyJ0IjoxNzEwMDAwMDAsImlkIjoiYWJjMTIzIn0=",
  "has_more": true
}

# 下一页
GET /v1/klines?symbol=BTCUSDT&interval=1m&limit=100&cursor=eyJ0IjoxNzEwMDAwMDAsImlkIjoiYWJjMTIzIn0=

时间型参数 since/until 常用于补历史数据。建议统一采用左闭右开区间:[since, until)。这样连续请求 [T1, T2) 与 [T2, T3) 不会重复 T2 时刻的数据,也不会漏掉。若接口文档写的是闭区间,必须在文档和代码中显式说明,否则重试时极易产生重复K线。

游标应包含排序键与唯一ID,并做 Base64 编码或签名。不要直接把数据库自增ID暴露为 cursor,否则容易被遍历和猜测。

四、案例:高频轮询中的分页选择

假设每 5 秒轮询一次最新成交,若使用 page=1&page_size=100,当 5 秒内新增 150 条时,第 1 页只能拿到最新 100 条,剩余 50 条被挤到第 2 页;如果客户端只请求第 1 页,就会漏数据。正确做法是记录上次最后一条的时间戳与ID,使用 since=last_ts&limit=500 持续拉取,直到返回条数小于 limit 为止。

last_ts = 0
while True:
    resp = get("/trades", params={"since": last_ts, "limit": 500})
    if not resp["data"]:
        break
    for item in resp["data"]:
        process(item)
    last_ts = resp["data"][-1]["ts"] + 1  # 左闭右开,避免重复
    if len(resp["data"]) < 500:
        break

这段代码体现了两个边界处理:一是 last_ts + 1 保证不重复;二是“返回条数小于 limit”作为终止条件,避免无限循环。若接口使用 cursor,则把 last_ts 换成 next_cursor,逻辑一致。

五、常见误区与复盘

  • 误区一:page 与 cursor 混用。同一接口同时支持两种分页,客户端切换时容易把 page 当成 cursor 传入,导致 400 或数据错乱。建议在参数校验层明确互斥。
  • 误区二:忽略 limit 上限。客户端传 limit=100000,服务端不截断,直接拖垮数据库。必须设置硬上限并返回实际生效值。
  • 误区三:since/until 边界不明确。文档写“包含”,代码写“不包含”,重试时同一秒数据重复入库。统一左闭右开并写入契约测试。
  • 误区四:cursor 无过期时间。长期有效的 cursor 可能指向已被清理的数据,导致查询报错。建议设置 TTL,并在失效时返回明确错误码。
  • 误区五:深分页不设限。offset=1000000 会扫描大量无用行。行情场景应改用游标或时间范围,并对 offset 设置最大值。
重试与分页必须一起设计:重试同一请求时,若使用 page/offset,可能因数据插入而拿到不同结果;若使用 cursor 或 since,重试是幂等的。因此,在限流与重试实战中,优先选择游标/时间型分页。

六、参数选型速查

静态历史数据、后台报表:page/page_size 足够;增量同步、高频轮询:cursor 或 since/until;需要按时间窗口补数:since/until 最直观;需要严格不重不漏:cursor + 唯一排序键。无论选哪种,都要在接口层做参数归一化、上限截断、边界校验和错误码约定,并在文档中写明区间开闭与默认值。

分页参数三组语义与边界处理 页码型 page / page_size 语义:第几页、每页多少条 风险:数据插入导致重复/遗漏 适用:静态快照、后台报表 偏移型 offset / limit 语义:跳过多少条、取多少条 风险:深分页扫描成本高 适用:小数据量、低频查询 游标/时间型 cursor / since / until 语义:从某位置之后继续取 优势:抗插入、可重试、幂等 适用:增量同步、高频轮询 边界处理规则 参数归一化 负数归零、超限截断 区间开闭 统一 [since, until) 游标校验 签名、TTL、格式校验 错误码 400/410 明确提示 重试场景优先选择 cursor / since,保证请求幂等,避免重复拉取同一页数据