分页查询

在排行榜系统中,分页查询是最常见的需求之一。用户通常只需要查看前 100 名或特定范围内的排名,而非整个榜单。然而,实现高效的分页查询并非易事,尤其当数据量达到百万级别时。

排行榜分页查询的挑战

传统的关系型数据库在处理分页查询时,通常使用 LIMIT offset, size 的方式。但当 offset 很大时,数据库需要扫描并跳过大量记录,导致性能急剧下降。

例如,查询第 10001 到 10020 名的用户:

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

这条查询需要扫描前 10000 条记录并丢弃它们,只返回最后的 20 条。随着页码增加,查询时间线性增长。

在排行榜场景中,这个问题更加严重:

  • 高频访问:用户频繁翻页查看排名
  • 实时性要求:排名可能每秒都在变化
  • 大数据量:热门榜单可能有数百万参与者

Redis ZRANGE 分页实现

Redis 的有序集合(Sorted Set)是实现排行榜的理想数据结构。ZRANGE 命令可以高效地获取指定排名范围内的成员。

基础分页查询

# 获取第 1 页(前 20 名),排名从 0 开始
ZRANGE rankings 0 19 WITHSCORES

# 获取第 2 页(20-39 名)
ZRANGE rankings 20 39 WITHSCORES

# 获取第 n 页(每页 20 条)
# start = (page - 1) * page_size
# end = start + page_size - 1
ZRANGE rankings {start} {end} WITHSCORES

倒序查询(从高到低)

排行榜通常按分数从高到低排序,使用 ZREVRANGE

# 获取前 100 名(分数从高到低)
ZREVRANGE rankings 0 99 WITHSCORES

# 获取第 5 页(80-99 名)
ZREVRANGE rankings 80 99 WITHSCORES

方案落地示例

深度分页问题及解决

当用户查询靠后的排名(如第 10000 页)时,即使是 Redis 也会面临性能问题。ZRANGE 需要遍历大量元素,导致响应时间增加。

问题表现

# 深度分页:获取第 10000 页(假设每页 20 条)
# 需要扫描 199980 个元素
ZREVRANGE rankings 199980 199999 WITHSCORES

随着偏移量增加:

  • 内存遍历成本上升
  • 网络传输数据量增加
  • 阻塞时间变长,影响其他请求

解决方案

1. 限制最大页码

最简单的方法是限制用户只能查看前 N 名:

大多数用户只关心前几百名,这个限制对体验影响很小。

2. 基于游标的分页

使用最后看到的分数或排名作为游标,避免计算偏移量:

3. 分段存储

将排行榜按排名范围分段存储到不同的 key 中:

# 分段存储
ranking:top1000      # 0-999 名
ranking:1000-9999    # 1000-9999 名
ranking:10000-plus   # 10000 名以后

# 查询时先确定段,再在段内分页
ZREVRANGE ranking:top1000 0 19 WITHSCORES

性能优化技巧

1. 使用 Pipeline 批量查询

当需要同时获取多个榜单或多次查询时,使用 Pipeline 减少网络往返:

2. 缓存热门页

对于访问频繁的页面(如首页),添加缓存层:

3. 预计算排名

对于不实时变化的榜单,可以预计算并存储分页结果:

4. 异步加载与懒加载

前端实现懒加载,用户滚动时才加载下一页:

总结

排行榜分页查询的优化需要综合考虑:

方案适用场景优点缺点
ZRANGE 基础分页前几千名查询简单高效深度分页慢
限制最大页码大部分用户场景实现简单无法查看深层排名
游标分页连续浏览场景避免偏移计算无法跳页
分段存储超大规模榜单分散查询压力管理复杂
缓存热门页高并发读取显著降低延迟需要处理缓存更新

在实际应用中,通常组合使用多种方案:限制最大查询范围 + 缓存热门页面 + 游标分页,以在性能和功能之间取得平衡。