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 批量拉取的整体流程
我把整个批量拉取的逻辑画成了一张图,方便你理解:
从图上你能看到,游标分页和偏移量分页在流程上的核心区别:游标分页每次返回一个「新游标」,下次请求带上它;偏移量分页则只返回数据,下次请求自己算偏移量。
4.5 实战中的批量拉取策略
说了这么多理论,来点实际的。我在项目中总结了一套批量拉取的「黄金法则」:
- 批量大小动态调整:不要写死 100 或 500。根据网络延迟、数据大小、内存压力动态调整。比如网络延迟高的时候,批量大一点;内存紧张的时候,批量小一点。
- 游标字段选型:优先用自增主键。如果没有,用「时间戳 + 业务唯一 ID」组合。千万别只用时间戳,同一毫秒的数据会让你头疼。
- 超时兜底:批量请求一定要设超时。我曾经见过一个系统,服务端挂了,客户端还在傻等,结果线程池全占满了。
- 幂等消费:即使用了游标分页,也要做好幂等。网络重试可能导致同一条数据被拉两次,消费端要做好去重。
血的教训:有一次我把游标分页的批量大小设成了 1000,结果服务端一次查询花了 5 秒,客户端超时重试,服务端负载直接飙到 100%。后来加了限流和熔断,才稳住。批量大小不是越大越好,得结合实际情况来。
好了,这一章的内容就这些。批量拉取的核心就三点:批量请求减少网络开销、游标分页保证数据一致性、动态调整适应不同场景。你把这些吃透了,消费端的拉取性能至少能翻一倍。