4、批量拉取策略:批量请求设计、分页拉取优化、游标分页 vs 偏移量分页

说到消费端的拉取性能,我第一个想到的就是「批量」。你想想看,一条一条地拉,跟一次拉一百条,网络开销差了多少倍?这章我们就来聊聊批量拉取的那些门道。

4.1 批量请求设计:别让网络成为瓶颈

很多新手容易犯一个错:循环里一条一条地发请求。每次请求都有 TCP 握手、TLS 握手、HTTP 头部传输……这些固定开销加起来,比数据本身还大。

核心原则:用一次请求的代价,换取 N 条数据的传输。

我习惯把批量请求设计成三个维度:

  • 批量大小(Batch Size):一次拉多少条?太小了浪费网络,太大了内存扛不住。
  • 批量间隔(Batch Interval):多久拉一次?实时性要求高的场景,间隔要短。
  • 批量超时(Batch Timeout):等不到满批怎么办?设个超时,到了就发。

举个例子,一个典型的批量拉取接口长这样:

// 批量拉取消息
POST /api/messages/batch
{
  "batch_size": 100,
  "timeout_ms": 200,
  "filters": {
    "type": "order",
    "status": "pending"
  }
}

返回的时候,服务端会尽量凑满 100 条,但如果 200 毫秒内没凑够,有多少发多少。这样既保证了吞吐,又控制了延迟。

我的经验:批量大小不是越大越好。我曾经把一个系统的批量从 50 调到 500,结果内存直接 OOM 了。后来发现,单条消息平均 10KB,500 条就是 5MB,并发 10 个消费者就是 50MB……嗯,得算清楚。

4.2 分页拉取优化:从「全量」到「增量」

分页拉取,说白了就是「一次拉不完,分几次拉」。但怎么分,差别很大。

我见过最原始的做法:每次拉 100 条,然后客户端自己拼。问题是,如果数据在拉的过程中有新增或删除,分页就会乱掉。

举个例子:

  • 第一页拉了 ID 1-100
  • 这时候新来了 10 条数据,ID 从 101 开始
  • 第二页拉 ID 101-200
  • 结果第一页和第二页之间,漏掉了那 10 条?

这就是典型的「幻读」问题。怎么解决?

注意:如果业务允许少量重复或遗漏,简单的偏移量分页就够了。但如果要求严格有序、不重不漏,必须用游标分页。

4.3 游标分页 vs 偏移量分页:选对工具

这两种分页方式,我做个对比表格,一目了然:

对比维度 偏移量分页(OFFSET) 游标分页(CURSOR)
实现方式 SELECT * FROM t LIMIT 10 OFFSET 100 SELECT * FROM t WHERE id > last_id LIMIT 10
性能 OFFSET 越大越慢(需要扫描前面所有行) 稳定 O(log N),与页数无关
数据一致性 新增/删除会导致数据偏移或重复 基于游标位置,不受新增/删除影响
适用场景 小数据量、静态数据、后台管理 大数据量、实时数据、消费端拉取
排序要求 支持任意排序 必须基于有序字段(如自增 ID、时间戳)

你看,游标分页在消费端拉取场景下几乎是碾压式的优势。为什么?因为消费端的数据通常是追加写入的,ID 或时间戳天然递增。用游标分页,每次拉取都从上次结束的位置继续,不会漏,也不会重复。

代码实现也很简单:

// 游标分页拉取
public List<Message> pullMessages(Long lastId, int batchSize) {
    String sql = "SELECT * FROM messages WHERE id > ? ORDER BY id ASC LIMIT ?";
    return jdbcTemplate.query(sql, lastId, batchSize);
}

// 第一次拉取,lastId = 0
// 第二次拉取,lastId = 上批最后一条的 id
// 以此类推……
避坑指南:我曾经用时间戳做游标,结果发现同一毫秒内产生了多条数据,游标就卡住了。后来改成「时间戳 + 自增 ID」组合游标,才彻底解决。记住:游标字段必须唯一且递增。

4.4 批量拉取的整体流程

我把整个批量拉取的逻辑画成了一张图,方便你理解:

批量拉取流程示意图 批量拉取核心流程 消费者发起批量请求 服务端接收请求参数 判断分页方式:游标 vs 偏移量 游标查询:WHERE id > ? 偏移量查询:LIMIT ? OFFSET ? 返回数据 + 新游标(last_id) 返回数据(可能偏移/重复) 游标分页适合消费端,偏移量分页适合后台管理

从图上你能看到,游标分页和偏移量分页在流程上的核心区别:游标分页每次返回一个「新游标」,下次请求带上它;偏移量分页则只返回数据,下次请求自己算偏移量。

4.5 实战中的批量拉取策略

说了这么多理论,来点实际的。我在项目中总结了一套批量拉取的「黄金法则」:

  1. 批量大小动态调整:不要写死 100 或 500。根据网络延迟、数据大小、内存压力动态调整。比如网络延迟高的时候,批量大一点;内存紧张的时候,批量小一点。
  2. 游标字段选型:优先用自增主键。如果没有,用「时间戳 + 业务唯一 ID」组合。千万别只用时间戳,同一毫秒的数据会让你头疼。
  3. 超时兜底:批量请求一定要设超时。我曾经见过一个系统,服务端挂了,客户端还在傻等,结果线程池全占满了。
  4. 幂等消费:即使用了游标分页,也要做好幂等。网络重试可能导致同一条数据被拉两次,消费端要做好去重。
血的教训:有一次我把游标分页的批量大小设成了 1000,结果服务端一次查询花了 5 秒,客户端超时重试,服务端负载直接飙到 100%。后来加了限流和熔断,才稳住。批量大小不是越大越好,得结合实际情况来。

好了,这一章的内容就这些。批量拉取的核心就三点:批量请求减少网络开销、游标分页保证数据一致性、动态调整适应不同场景。你把这些吃透了,消费端的拉取性能至少能翻一倍。