这是 Beta 探索课程,内容结构、实验步骤和示例可能会继续调整。
分页查询
在排行榜系统中,分页查询是最常见的需求之一。用户通常只需要查看前 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 基础分页 | 前几千名查询 | 简单高效 | 深度分页慢 |
| 限制最大页码 | 大部分用户场景 | 实现简单 | 无法查看深层排名 |
| 游标分页 | 连续浏览场景 | 避免偏移计算 | 无法跳页 |
| 分段存储 | 超大规模榜单 | 分散查询压力 | 管理复杂 |
| 缓存热门页 | 高并发读取 | 显著降低延迟 | 需要处理缓存更新 |
在实际应用中,通常组合使用多种方案:限制最大查询范围 + 缓存热门页面 + 游标分页,以在性能和功能之间取得平衡。