排行榜系统:Redis ZSet 与分片计算的设计 — editorial cover photo

排行榜系统:Redis ZSet 与分片计算的设计

实时排名、Top N、亿级用户排行,ZSet 是起点但不是终点。数据量上来之后分片和实时计算的取舍才是难点。

2025年5月6日·返回文章列表
文章大纲

排行榜是游戏和社交应用里的高频需求。战力榜、消费榜、签到榜、点赞榜,每个业务都想搞个榜来刺激活跃。小规模的时候,排行榜特别好做,Redis ZSet 几行代码搞定。规模一大,问题就全冒出来了。

亿级用户的排行榜,全塞进一个 ZSet 内存扛不住。实时更新的榜单,每次变化都要重排,CPU 扛不住。跨维度的榜单(比如「我的好友里排第几」),单纯 ZSet 算不出来。这篇讲排行榜从简单到复杂的演进,以及每一步要做的取舍。

ZSet,排行榜的基石

PlantUML
正在加载图表…
PlantUML diagram

图:ZSet 双结构原理——跳表负责排序取 Top N,哈希表负责按 member 查 score,两者协作覆盖排行榜操作

Redis 的 ZSet(有序集合)是做排行榜的天然工具。每个成员有个 score,ZSet 按 score 排序,提供 ZADD(加分)、ZSCORE(查分)、ZRANK(查排名)、ZREVRANGE(取 Top N)这些操作,正好覆盖排行榜的核心需求。

一个游戏战力榜,用户战力变化时 ZADD leaderboard {userId} {score} 更新。查 Top 100 用 ZREVRANGE leaderboard 0 99 WITHSCORES。查某用户排名用 ZREVRANK leaderboard {userId}。简单、实时、高效。

ZSet 的内部实现是跳表(skiplist)加哈希表,插入和查询都是 O(logN)。对于百万级成员的榜单,单次操作在毫秒级,性能完全够。

这套方案能扛到多大规模。经验上,单个 ZSet 成员数在百万级以内,Redis 单实例轻松搞定。到了千万级,内存压力开始显现(每个成员要存成员值、score、跳表指针,一个成员几十到上百字节,千万级就是 GB 级),而且大 key 的操作(比如全量 ZRANGE)会阻塞 Redis。到亿级,单实例完全扛不住。

所以亿级排行榜的核心问题不是「怎么排序」,是「怎么把排序拆开」。

分片,突破单实例的内存和算力

PlantUML
正在加载图表…
PlantUML diagram

图:分片归并算全局排名——并行 ZCOUNT 求和算排名,多路归并取 Top N,用快照缓存降低延迟

亿级排行榜必须分片。基本思路是,把用户按某个规则(通常是 ID 哈希)分散到多个 ZSet(多个 Redis 实例),每个分片维护自己那部分用户的局部排名。

但分片带来一个新问题,全局排名怎么算。某个用户在全局排第几,不能只看他在自己分片里的排名,因为别的分片里可能有很多比他分高的。

最朴素的全局排名算法是这样。要查用户 U 的全局排名,去每个分片查「比分高的有多少人」(用 ZCOUNT 查 score 大于 U 的数量),把各分片的结果加起来,就是 U 的全局排名。这个查询要访问所有分片,N 个分片就是 N 次 ZCOUNT,延迟随分片数线性增长。

优化思路是缓存。把每个分片的「score 分布」定期快照下来(比如每分钟),全局排名查询基于快照算,不用每次都打所有分片。代价是排名有几秒到一分钟的延迟,但排行榜这种场景,延迟通常可接受。

Top N 的查询更复杂。要从所有分片里各取 Top N,再合并排序取全局 Top N。这是一个归并排序的过程。如果 N 不大(比如 Top 100),开销可控。如果 N 很大,要权衡。

实时 vs 近实时,一个关键取舍

PlantUML
正在加载图表…
PlantUML diagram

图:实时 vs 近实时对比——实时体验好但成本高,近实时压可控,绝大多数业务近实时就够

排行榜的一个核心取舍是,要实时还是近实时。

实时榜单,每次分数变化立刻反映到排名。优点是用户体验好,缺点是写压力大(每次变化都要更新 ZSet),而且全局排名的查询贵(要打所有分片)。

近实时榜单,分数变化先攒着,每隔一段时间(比如 1 分钟)批量更新和重算排名。优点是压力可控(批量处理比逐条高效),缺点是排名有延迟(用户看到的可能是一分钟前的)。

绝大多数业务场景,近实时就够了。用户不会盯着排行榜看实时变化,差一分钟无所谓。而且批量更新能大幅降低写压力,对系统的稳定性是好事。

真正需要实时的场景不多,比如直播间的「打赏榜」在主播pk的时候要实时跳动。这种场景值得为实时性付出更高的工程成本,但绝大多数榜单不值得。

不同类型的榜单,方案不同

榜单按维度分几种,方案差别很大。

全局榜单,所有用户一起排。规模大,用分片加归并。规模小,单 ZSet 搞定。

好友榜单,只看我和我的好友之间的排名。这个用 ZSet 不好做,因为每个用户的好友圈不同,没法预先排好。常见的做法是,查某个用户的好友榜单时,取出他的好友列表,从全局 ZSet 里查每个人的 score,在应用层排序。这个查询是 O(好友数 × logN),好友数不大(几百)的话可以接受。

圈子榜单,比如「我所在的城市排第几」「我这个年龄段排第几」。这种维度太多,没法每个维度都维护一个 ZSet。常见的折中是,只维护几个最重要的全局榜单,圈子榜排名实时计算或者用预聚合的统计数据近似。

时间窗口榜单,比如「本周战力榜」「本月消费榜」。这种需要定期清零或者滚动。常见做法是按周期建不同的 ZSet(比如 leaderboard:2026W27),周期结束后归档。实时性要求高的,用滑动窗口,但这会让 ZSet 的维护更复杂。

几个工程上的坑

做排行榜踩过几个坑值得分享。

大 key 问题。单个 ZSet 成员太多(千万级),ZRANGE 全量取会阻塞 Redis 几秒。生产环境要禁止全量操作,只取 Top N。或者分片把单 key 控制在合理大小。

热点更新问题。某些用户的分数变化极频繁(比如主播打赏),每次变化都 ZADD 会把这个 key 打成热点。可以用「客户端攒一批再批量更新」来降低写频率,接受秒级延迟。

排名跳变问题。近实时榜单在重算的时候,排名可能大幅跳变(比如某用户突然冲到第一),用户体验不好。可以在前端做平滑过渡,限制排名变化的幅度。

别为了「实时」过度设计

排行榜最常见的过度设计是为了「实时性」上复杂的实时计算框架。实际上绝大多数榜单用分钟级的批量更新就够了,ZSet 加定时重算,简单稳定成本低。只有那些真需要秒级实时的少数场景(直播打赏pk),才值得上实时方案。先问业务能不能容忍延迟,能容忍就别上重的方案。排行榜的价值在于「展示和激励」,不在于「精确到秒的实时」,别把精力花在用户感知不到的精度上。

排行榜这个小需求,折射出高并发系统的几个经典课题。数据量增长时的分片、实时和近实时的取舍、不同维度查询的复杂度、热点和大 key 的治理。把这些想透了,不光排行榜能做好,类似的「实时聚合统计」类需求都能举一反三。

Continue Reading

关联文档推荐

查看全部
库存系统:预占、扣减与一致性保障 — editorial cover photo秒杀系统设计:库存防超卖与流量削峰全链路 — editorial cover photo限流算法全景:从计数器到令牌桶的工程实现 — editorial cover photo
作者:archy.shawn
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。